Skip to content
Draft
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
49 changes: 49 additions & 0 deletions docs/plans/app-test-typescript-refactor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Original Prompt

> 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.

# Goal

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.

# Non-Goals

- Full repo-wide JavaScript-to-TypeScript conversion in one pass.
- Replacing the scan engine implementation without test coverage first.
- Changing user-facing app-test behavior beyond what is needed to make the feature reliable.

# Repo Findings

- 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).
- 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).
- 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).
- A newer worker-based scanner path exists behind `?version=2`, but the production page still defaults to the legacy path.

# Decision

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.

# Rollout Plan

1. Add typed Playwright support for spinning up Astro and generating a known-good app archive fixture.
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.
3. Fix app-test regressions exposed by the browser test.
4. Start the TypeScript conversion with the new Playwright support layer and continue into the scanner path in later passes.

# Validation Gates

- `pnpm test:browser test/playwright/apple-silicon-app-test.playwright.ts`
- `pnpm test:browser`
- Manual smoke check of `/apple-silicon-app-test/` if the browser test exposes timing or hydration issues

# Deliverables

- A Playwright browser test covering the app-test upload and scan flow
- Any app-test fixes required to make that test pass
- Initial TypeScript refactor scaffolding in the browser-test/scanner-adjacent path

# Risks And Open Questions

- The legacy scanner depends on zip and Mach-O parsing behavior in the browser, so fixture choice needs to stay minimal and deterministic.
- The repo still mixes `.js`, `.mjs`, `.ts`, `.vue`, and `.astro`, so conversion order matters; scanner-adjacent modules should move only after coverage exists.
- The worker-based scanner path likely needs separate follow-up coverage before it can replace the legacy path.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
"test-vitest": "vitest",
"test": "vitest run",
"test:browser": "vitest run --config vitest.playwright.config.mjs",
"test:browser:pagefind": "vitest run --config vitest.playwright.config.mjs test/playwright/pagefind-native-filter.playwright.js",
"test:browser:pagefind:live": "PLAYWRIGHT_BASE_URL=https://doesitarm.com vitest run --config vitest.playwright.config.mjs test/playwright/pagefind-native-filter.playwright.js",
"test:browser:pagefind": "vitest run --config vitest.playwright.config.mjs test/playwright/pagefind-native-filter.playwright.ts",
"test:browser:pagefind:live": "PLAYWRIGHT_BASE_URL=https://doesitarm.com vitest run --config vitest.playwright.config.mjs test/playwright/pagefind-native-filter.playwright.ts",
"dev": "pnpm run dev-astro",
"build": "pnpm run generate-astro",
"build-api": "pnpm run clone-readme && pnpm exec vite-node build-lists.js -- --with-api --no-lists",
Expand Down
165 changes: 165 additions & 0 deletions test/playwright/apple-silicon-app-test.playwright.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import type { Browser, Page } from 'playwright-core'
import {
afterAll,
beforeAll,
describe,
expect,
it
} from 'vitest'

import {
launchBrowser,
startAstroDevServer,
stopChildProcess,
type AstroDevServer
} from './support/astro-browser-test'
import {
createNativeAppArchive,
type PlaywrightUploadFile
} from './support/app-archive-fixture'

describe( 'Apple Silicon app test page', () => {
let browser: Browser
let devServer: AstroDevServer
let appArchive: PlaywrightUploadFile

beforeAll( async () => {
appArchive = await createNativeAppArchive()

devServer = await startAstroDevServer({
env: {
TEST_RESULT_STORE: '/api/test-results'
},
preferConfiguredBaseUrl: false
})

browser = await launchBrowser()
await warmAppTestRoute( browser, devServer.baseUrl )
} )

afterAll( async () => {
await browser?.close()
await stopChildProcess( devServer?.process || null )
} )

it( 'uploads an app archive, scans it, and renders a native result', async () => {
const page = await browser.newPage()
const consoleErrors: string[] = []
const pageErrors: string[] = []
const submittedScans: Record<string, unknown>[] = []

page.on( 'console', message => {
if ( message.type() === 'error' ) {
consoleErrors.push( message.text() )
}
} )

page.on( 'pageerror', error => {
pageErrors.push( error.message )
} )

await stubResultStore( page, submittedScans )

await page.goto( `${ devServer.baseUrl }/apple-silicon-app-test/`, {
waitUntil: 'load'
} )

await page.waitForFunction( () => {
const island = document.querySelector( 'astro-island[component-url="/pages/apple-silicon-app-test.vue"]' )

return Boolean( island && !island.hasAttribute( 'ssr' ) )
}, {
timeout: 30 * 1000
} )

await page.locator( 'input[type="file"]' ).setInputFiles( appArchive )
await waitForBodyText( page, 'Total Files: 1', {
consoleErrors,
devServerOutput: devServer.output.text,
pageErrors
} )

const firstScanRow = page.locator( '.results-container li' ).first()

await waitForBodyText( page, 'Playwright Native App', {
consoleErrors,
devServerOutput: devServer.output.text,
pageErrors
} )
await waitForBodyText( page, '✅ This app is natively compatible with Apple Silicon!', {
consoleErrors,
devServerOutput: devServer.output.text,
pageErrors
} )

await firstScanRow.locator( 'summary' ).click()

const rowText = await firstScanRow.textContent()

expect( rowText ).toContain( 'Bundle Identifier' )
expect( rowText ).toContain( 'com.doesitarm.playwright-native-app' )

expect( submittedScans.length, devServer.output.text ).toBe( 1 )
expect( submittedScans[ 0 ]?.filename, JSON.stringify( submittedScans[ 0 ] ) ).toBe( 'Playwright Native App.app.zip' )
expect( submittedScans[ 0 ]?.result, JSON.stringify( submittedScans[ 0 ] ) ).toBe( '✅' )
expect( pageErrors, devServer.output.text ).toEqual( [] )
expect( consoleErrors, devServer.output.text ).toEqual( [] )
} )
} )

async function stubResultStore ( page: Page, submittedScans: Record<string, unknown>[] ) {
await page.route( '**/api/test-results', async route => {
const postData = route.request().postDataJSON()

if ( postData && typeof postData === 'object' ) {
submittedScans.push( postData as Record<string, unknown> )
}

await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
supportedVersionNumber: null
})
})
} )
}

async function waitForBodyText ( page: Page, expectedText: string, debugContext: {
consoleErrors: string[]
devServerOutput: string
pageErrors: string[]
} ) {
try {
await page.waitForFunction( textToFind => {
return Boolean( document.body?.textContent?.includes( textToFind ) )
}, expectedText, {
timeout: 30 * 1000
} )
} catch ( error ) {
const bodyText = await page.locator( 'body' ).textContent()

throw new Error( [
`Timed out waiting for body text: ${ expectedText }`,
bodyText || '',
debugContext.pageErrors.join( '\n' ),
debugContext.consoleErrors.join( '\n' ),
debugContext.devServerOutput
].filter( Boolean ).join( '\n\n' ), {
cause: error
} )
}
}

async function warmAppTestRoute ( browser: Browser, baseUrl: string ) {
const warmPage = await browser.newPage()

try {
await warmPage.goto( `${ baseUrl }/apple-silicon-app-test/`, {
waitUntil: 'load'
} )
await warmPage.waitForTimeout( 5000 )
} finally {
await warmPage.close()
}
}
Loading
Loading