Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions e2e/fixtures/test-codebook.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
name,label,type
income,Household income,continuous
district,District identifier,identifier
edu_rate,Education rate,continuous
urban_pct,Urban percentage,continuous
survey_weight,Survey sampling weight,continuous
dir_est,Direct estimate of mean income,continuous
dir_var,Sampling variance of direct estimate,continuous
96 changes: 96 additions & 0 deletions e2e/wizard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { test, expect, type Page } from '@playwright/test'
import path from 'path'
import { fileURLToPath } from 'url'

const __dirname = path.dirname(fileURLToPath(import.meta.url))

// Helper to select an option by label text in a <select> that is the ONLY
// select in the row whose first cell contains the given variable name.
async function setRole(page: Page, varName: string, role: string) {
const row = page.locator('table tbody tr').filter({ hasText: varName }).first()
await row.locator('select').first().selectOption({ label: role })
}

test.describe('Wizard happy path', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
})

test('full flow: upload codebook → assign roles → set flags → find FH → download R', async ({ page }) => {
// ── Step 1: Upload codebook ─────────────────────────────────────────────
const fileInput = page.locator('[data-testid="file-input"]')
const fixturePath = path.resolve(__dirname, 'fixtures/test-codebook.csv')
await fileInput.setInputFiles(fixturePath)

// Verify variables appear in the preview table (exact match on the name cell)
await expect(page.getByText('income', { exact: true }).first()).toBeVisible()
await expect(page.getByText('district', { exact: true }).first()).toBeVisible()

// Advance to Step 2
await page.getByRole('button', { name: /next/i }).click()

// ── Step 2: Assign roles ────────────────────────────────────────────────
await expect(page.getByText('Variable roles')).toBeVisible()

// income → target
await setRole(page, 'income', 'Target')
// district is auto-suggested as area-id (type=identifier), confirm it
// edu_rate → auxiliary (default auto-suggest is 'ignored', set manually)
await setRole(page, 'edu_rate', 'Auxiliary')
await setRole(page, 'urban_pct', 'Auxiliary')
await setRole(page, 'survey_weight', 'Survey weight')
await setRole(page, 'dir_est', 'Direct estimate')
await setRole(page, 'dir_var', 'Sampling variance')

// Advance to Step 3
await page.getByRole('button', { name: /next/i }).click()

// ── Step 3: Data availability ───────────────────────────────────────────
await expect(page.getByText('Data availability')).toBeVisible()

// Tick: microdata, area aggregates, weights, census auxiliaries (so FH and BHF both eligible)
const checkByLabel = (label: string) =>
page.getByText(label, { exact: false }).first().click()

await checkByLabel('individual household/unit records')
await checkByLabel('area-level direct estimates')
await checkByLabel('sampling weights')
await checkByLabel('population-level auxiliary variables')

// Stata version: already defaults to 14; no change needed
// No spatial, no outliers (leave unchecked)

// Advance to Step 4
await page.getByRole('button', { name: /next/i }).click()

// ── Step 4: Methods ─────────────────────────────────────────────────────
await expect(page.getByText('Recommended methods')).toBeVisible()

// At least one method card is shown
const methodCards = page.locator('[data-testid^="select-"]')
await expect(methodCards.first()).toBeVisible()

// FH-EBLUP card is present (eligible because area aggregates + census auxiliaries)
const fhRadio = page.locator('[data-testid="select-fh-eblup"]')
await expect(fhRadio).toBeVisible()

// Select FH-EBLUP
await fhRadio.click()

// Advance to Step 5
await page.getByRole('button', { name: /next/i }).click()

// ── Step 5: Download ────────────────────────────────────────────────────
await expect(page.getByText('Download scripts')).toBeVisible()

// Verify R script preview contains expected content
const pre = page.locator('pre').first()
await expect(pre).toContainText('library(')

// Download R script and verify a file download is triggered
const downloadPromise = page.waitForEvent('download')
await page.locator('[data-testid="download-r-fh-eblup"]').click()
const dl = await downloadPromise
expect(dl.suggestedFilename()).toContain('.R')
})
})
32 changes: 32 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"lint": "eslint . --max-warnings 0"
},
"dependencies": {
"@types/papaparse": "^5.5.2",
"papaparse": "^5.5.3",
"react": "^19.2.7",
"react-dom": "^19.2.7"
},
Expand Down
13 changes: 3 additions & 10 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import { Wizard } from './wizard/Wizard.js'

function App() {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<h1 className="text-3xl font-bold text-gray-900">SAE Syntax Generator</h1>
<p className="mt-2 text-gray-600">
Small Area Estimation script generator — wizard coming in Phase 4.
</p>
</div>
</div>
)
return <Wizard />
}

export default App
2 changes: 1 addition & 1 deletion src/engine/codegen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const FH_INPUTS: UserInputs = {
directEstVar: 'dir_est',
directVarVar: 'dir_var',
surveyDataPath: 'survey.csv',
censusDataPath: 'area_data.csv',
areaDataPath: 'area_data.csv', // FH is area-level: uses {{AREA_DATA}} token
stataVersion: 14,
nSimulations: 200,
mseMethod: 'analytic',
Expand Down
8 changes: 5 additions & 3 deletions src/engine/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export interface UserInputs {
coordinateLat?: string
coordinateLon?: string
contiguityMatrixPath?: string
censusDataPath?: string
censusDataPath?: string // unit-level census microdata (EBP, BHF, ELL, HB-unit)
areaDataPath?: string // area-level file with direct estimates + area auxiliaries (FH, Spatial-FH, GREG)
surveyDataPath?: string
stataVersion: number
nSimulations?: number
Expand All @@ -33,6 +34,7 @@ function buildTokenMap(inputs: UserInputs): Record<string, string> {
const auxStata = inputs.auxiliaryVars.join(' ')
const survey = inputs.surveyDataPath ?? 'survey.csv'
const census = inputs.censusDataPath ?? 'census.csv'
const area = inputs.areaDataPath ?? 'area_data.csv'

return {
DATE: date,
Expand All @@ -45,8 +47,8 @@ function buildTokenMap(inputs: UserInputs): Record<string, string> {
DIRECT_EST_VAR: inputs.directEstVar ?? '',
DIRECT_VAR_VAR: inputs.directVarVar ?? '',
SURVEY_DATA: survey,
CENSUS_DATA: census,
AREA_DATA: census, // area-level aggregates default to the same path as census
CENSUS_DATA: census, // unit-level census microdata
AREA_DATA: area, // area-level aggregates (direct estimates + area auxiliaries)
CONTIG_MATRIX: inputs.contiguityMatrixPath ?? 'contiguity_matrix.csv',
N_SIM: String(inputs.nSimulations ?? 200),
MSE_METHOD: inputs.mseMethod ?? 'bootstrap',
Expand Down
Loading
Loading