Skip to content

Commit 0480c47

Browse files
committed
test(playwright): lock browser coverage before scanner refactors
Add a typed Playwright harness for Pagefind and the Apple Silicon app-test flow so scanner work has browser-level protection. Keep the rollout plan in the same stack so the TypeScript conversion stays staged and reviewable. Constraint: Must not change production runtime behavior in this commit Rejected: Leave the old JS browser test and add a second harness | duplicates setup and leaves the targeted browser script broken Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep browser-only helpers under test/playwright/support until the runtime scanner surface is fully typed Tested: pnpm run typecheck; pnpm run test:browser; pnpm run test:browser:pagefind Not-tested: Live browser checks against doesitarm.com
1 parent c5ec942 commit 0480c47

8 files changed

Lines changed: 633 additions & 276 deletions
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Original Prompt
2+
3+
> OK, this is really great. I've been wanting to do a TypeScript conversion for this repo for a while. Tell me about that. We also need to... I want-- we have a-- the app test feature. Every time I try to touch that code, it gets fragile. So build a playwright test to verify that feature so that way we can scan apps, and then get that working. and then let's start a refactor. But once you have that working and verified, Let's start to refactor to get this converted all to TypeScript.
4+
5+
# Goal
6+
7+
Lock the Apple Silicon app-test flow with an end-to-end browser test, fix regressions in the real scan/upload path, and begin the TypeScript conversion with small, reviewable changes around the browser-test and scanner surface.
8+
9+
# Non-Goals
10+
11+
- Full repo-wide JavaScript-to-TypeScript conversion in one pass.
12+
- Replacing the scan engine implementation without test coverage first.
13+
- Changing user-facing app-test behavior beyond what is needed to make the feature reliable.
14+
15+
# Repo Findings
16+
17+
- The app-test UI is implemented in [pages/apple-silicon-app-test.vue](/Users/athena/Code/doesitarm/pages/apple-silicon-app-test.vue) and mounted by [src/pages/apple-silicon-app-test.astro](/Users/athena/Code/doesitarm/src/pages/apple-silicon-app-test.astro).
18+
- The current browser-test harness exists, but only covers Pagefind in [test/playwright/pagefind-native-filter.playwright.js](/Users/athena/Code/doesitarm/test/playwright/pagefind-native-filter.playwright.js).
19+
- The app-test flow depends on archive extraction, plist parsing, Mach-O parsing, and an HTTP POST to `TEST_RESULT_STORE` via [helpers/app-files-scanner.js](/Users/athena/Code/doesitarm/helpers/app-files-scanner.js).
20+
- A newer worker-based scanner path exists behind `?version=2`, but the production page still defaults to the legacy path.
21+
22+
# Decision
23+
24+
Add a deterministic Playwright upload test that scans a generated zipped `.app` bundle against the real page, stub only the remote result-store POST, and use that as the safety rail before starting TypeScript refactors.
25+
26+
# Rollout Plan
27+
28+
1. Add typed Playwright support for spinning up Astro and generating a known-good app archive fixture.
29+
2. Add a browser test for `/apple-silicon-app-test/` that uploads the fixture, intercepts the result-store request, and asserts the rendered native result.
30+
3. Fix app-test regressions exposed by the browser test.
31+
4. Start the TypeScript conversion with the new Playwright support layer and continue into the scanner path in later passes.
32+
33+
# Validation Gates
34+
35+
- `pnpm test:browser test/playwright/apple-silicon-app-test.playwright.ts`
36+
- `pnpm test:browser`
37+
- Manual smoke check of `/apple-silicon-app-test/` if the browser test exposes timing or hydration issues
38+
39+
# Deliverables
40+
41+
- A Playwright browser test covering the app-test upload and scan flow
42+
- Any app-test fixes required to make that test pass
43+
- Initial TypeScript refactor scaffolding in the browser-test/scanner-adjacent path
44+
45+
# Risks And Open Questions
46+
47+
- The legacy scanner depends on zip and Mach-O parsing behavior in the browser, so fixture choice needs to stay minimal and deterministic.
48+
- The repo still mixes `.js`, `.mjs`, `.ts`, `.vue`, and `.astro`, so conversion order matters; scanner-adjacent modules should move only after coverage exists.
49+
- The worker-based scanner path likely needs separate follow-up coverage before it can replace the legacy path.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131
"test-vitest": "vitest",
3232
"test": "vitest run",
3333
"test:browser": "vitest run --config vitest.playwright.config.mjs",
34-
"test:browser:pagefind": "vitest run --config vitest.playwright.config.mjs test/playwright/pagefind-native-filter.playwright.js",
35-
"test:browser:pagefind:live": "PLAYWRIGHT_BASE_URL=https://doesitarm.com vitest run --config vitest.playwright.config.mjs test/playwright/pagefind-native-filter.playwright.js",
34+
"test:browser:pagefind": "vitest run --config vitest.playwright.config.mjs test/playwright/pagefind-native-filter.playwright.ts",
35+
"test:browser:pagefind:live": "PLAYWRIGHT_BASE_URL=https://doesitarm.com vitest run --config vitest.playwright.config.mjs test/playwright/pagefind-native-filter.playwright.ts",
3636
"dev": "pnpm run dev-astro",
3737
"build": "pnpm run generate-astro",
3838
"build-api": "pnpm run clone-readme && pnpm exec vite-node build-lists.js -- --with-api --no-lists",
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import type { Browser, Page } from 'playwright-core'
2+
import {
3+
afterAll,
4+
beforeAll,
5+
describe,
6+
expect,
7+
it
8+
} from 'vitest'
9+
10+
import {
11+
launchBrowser,
12+
startAstroDevServer,
13+
stopChildProcess,
14+
type AstroDevServer
15+
} from './support/astro-browser-test'
16+
import {
17+
createNativeAppArchive,
18+
type PlaywrightUploadFile
19+
} from './support/app-archive-fixture'
20+
21+
const appTestVariants = [
22+
{
23+
name: 'legacy scanner',
24+
routeSuffix: ''
25+
},
26+
{
27+
name: 'worker scanner',
28+
routeSuffix: '?version=2'
29+
}
30+
] as const
31+
32+
describe( 'Apple Silicon app test page', () => {
33+
let browser: Browser
34+
let devServer: AstroDevServer
35+
let appArchive: PlaywrightUploadFile
36+
37+
beforeAll( async () => {
38+
appArchive = await createNativeAppArchive()
39+
40+
devServer = await startAstroDevServer({
41+
env: {
42+
TEST_RESULT_STORE: '/api/test-results'
43+
},
44+
preferConfiguredBaseUrl: false
45+
})
46+
47+
browser = await launchBrowser()
48+
await Promise.all( appTestVariants.map( variant => {
49+
return warmAppTestRoute( browser, devServer.baseUrl, variant.routeSuffix )
50+
} ) )
51+
} )
52+
53+
afterAll( async () => {
54+
await browser?.close()
55+
await stopChildProcess( devServer?.process || null )
56+
} )
57+
58+
it.each( appTestVariants )( 'uploads an app archive through the %s path and renders a native result', async ( variant ) => {
59+
const page = await browser.newPage()
60+
const consoleErrors: string[] = []
61+
const pageErrors: string[] = []
62+
const submittedScans: Record<string, unknown>[] = []
63+
64+
page.on( 'console', message => {
65+
if ( message.type() === 'error' ) {
66+
consoleErrors.push( message.text() )
67+
}
68+
} )
69+
70+
page.on( 'pageerror', error => {
71+
pageErrors.push( error.message )
72+
} )
73+
74+
await stubResultStore( page, submittedScans )
75+
76+
await page.goto( `${ devServer.baseUrl }/apple-silicon-app-test/${ variant.routeSuffix }`, {
77+
waitUntil: 'load'
78+
} )
79+
80+
await page.waitForFunction( () => {
81+
const island = document.querySelector( 'astro-island[component-url="/pages/apple-silicon-app-test.vue"]' )
82+
83+
return Boolean( island && !island.hasAttribute( 'ssr' ) )
84+
}, {
85+
timeout: 30 * 1000
86+
} )
87+
88+
await page.locator( 'input[type="file"]' ).setInputFiles( appArchive )
89+
await waitForBodyText( page, 'Total Files: 1', {
90+
consoleErrors,
91+
devServerOutput: devServer.output.text,
92+
pageErrors
93+
} )
94+
95+
const firstScanRow = page.locator( '.results-container li' ).first()
96+
97+
await waitForBodyText( page, 'Playwright Native App', {
98+
consoleErrors,
99+
devServerOutput: devServer.output.text,
100+
pageErrors
101+
} )
102+
await waitForBodyText( page, '✅ This app is natively compatible with Apple Silicon!', {
103+
consoleErrors,
104+
devServerOutput: devServer.output.text,
105+
pageErrors
106+
} )
107+
108+
await firstScanRow.locator( 'summary' ).click()
109+
110+
const rowText = await firstScanRow.textContent()
111+
112+
expect( rowText ).toContain( 'Bundle Identifier' )
113+
expect( rowText ).toContain( 'com.doesitarm.playwright-native-app' )
114+
115+
expect( submittedScans.length, devServer.output.text ).toBe( 1 )
116+
expect( submittedScans[ 0 ]?.filename, JSON.stringify( submittedScans[ 0 ] ) ).toBe( 'Playwright Native App.app.zip' )
117+
expect( submittedScans[ 0 ]?.result, JSON.stringify( submittedScans[ 0 ] ) ).toBe( '✅' )
118+
expect( pageErrors, devServer.output.text ).toEqual( [] )
119+
expect( consoleErrors, devServer.output.text ).toEqual( [] )
120+
} )
121+
} )
122+
123+
async function stubResultStore ( page: Page, submittedScans: Record<string, unknown>[] ) {
124+
await page.route( '**/api/test-results', async route => {
125+
const postData = route.request().postDataJSON()
126+
127+
if ( postData && typeof postData === 'object' ) {
128+
submittedScans.push( postData as Record<string, unknown> )
129+
}
130+
131+
await route.fulfill({
132+
status: 200,
133+
contentType: 'application/json',
134+
body: JSON.stringify({
135+
supportedVersionNumber: null
136+
})
137+
})
138+
} )
139+
}
140+
141+
async function waitForBodyText ( page: Page, expectedText: string, debugContext: {
142+
consoleErrors: string[]
143+
devServerOutput: string
144+
pageErrors: string[]
145+
} ) {
146+
try {
147+
await page.waitForFunction( textToFind => {
148+
return Boolean( document.body?.textContent?.includes( textToFind ) )
149+
}, expectedText, {
150+
timeout: 30 * 1000
151+
} )
152+
} catch ( error ) {
153+
const bodyText = await page.locator( 'body' ).textContent()
154+
155+
throw new Error( [
156+
`Timed out waiting for body text: ${ expectedText }`,
157+
bodyText || '',
158+
debugContext.pageErrors.join( '\n' ),
159+
debugContext.consoleErrors.join( '\n' ),
160+
debugContext.devServerOutput
161+
].filter( Boolean ).join( '\n\n' ), {
162+
cause: error
163+
} )
164+
}
165+
}
166+
167+
async function warmAppTestRoute ( browser: Browser, baseUrl: string, routeSuffix = '' ) {
168+
const warmPage = await browser.newPage()
169+
170+
try {
171+
await warmPage.goto( `${ baseUrl }/apple-silicon-app-test/${ routeSuffix }`, {
172+
waitUntil: 'load'
173+
} )
174+
await warmPage.waitForTimeout( 5000 )
175+
} finally {
176+
await warmPage.close()
177+
}
178+
}

0 commit comments

Comments
 (0)