-
Notifications
You must be signed in to change notification settings - Fork 715
test: establish E2E foundation with initial smoke coverage #830
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
a361d35
a9b6119
c4d9080
f1508f8
954905d
e30c5b1
9c4a98d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| FROM node:22.22.2-bookworm | ||
|
|
||
| WORKDIR /app/nav-app | ||
| ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright | ||
| COPY nav-app/package.json nav-app/package-lock.json ./ | ||
| RUN npm ci --ignore-scripts | ||
| RUN --mount=type=secret,id=extra_ca \ | ||
| if [ -f /run/secrets/extra_ca ]; then export NODE_EXTRA_CA_CERTS=/run/secrets/extra_ca; fi; \ | ||
| ./node_modules/.bin/playwright install --with-deps chromium --only-shell | ||
|
|
||
| COPY nav-app/angular.json nav-app/tsconfig*.json nav-app/playwright.config.ts ./ | ||
| COPY nav-app/src ./src | ||
| COPY nav-app/e2e ./e2e | ||
| COPY layers /app/layers | ||
| COPY *.md /app/ | ||
| RUN npm run build -- --configuration production --aot=false | ||
|
Check warning on line 16 in nav-app/e2e/Dockerfile
|
||
|
|
||
| RUN mkdir -p tmp && chown node:node tmp | ||
| USER node | ||
|
|
||
| CMD ["npm", "run", "test:e2e"] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| ** | ||
| !*.md | ||
| !layers/ | ||
| !layers/** | ||
| !nav-app/ | ||
| !nav-app/package.json | ||
| !nav-app/package-lock.json | ||
| !nav-app/angular.json | ||
| !nav-app/tsconfig*.json | ||
| !nav-app/src/ | ||
| !nav-app/src/** | ||
| !nav-app/playwright.config.ts | ||
| !nav-app/e2e/ | ||
| !nav-app/e2e/** |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| # End-to-end tests | ||
|
|
||
| Playwright runs the E2E suite in Chromium. | ||
|
|
||
| Run from `nav-app` with Node 22, npm, and Python 3 available: | ||
|
|
||
| ```sh | ||
| npm ci --ignore-scripts | ||
| npx playwright install chromium --only-shell | ||
| npm run build -- --configuration production --aot=false | ||
| npm run test:e2e | ||
| ``` | ||
|
|
||
| Playwright starts and stops a local static server on port 4173. The suite uses the | ||
| production build in `dist/browser`; rebuild after application changes. | ||
|
|
||
| Downloaded JSON and failure traces are saved under `tmp/playwright/test-results`. | ||
| There are no retries. The CI quality gate runs the suite after the production | ||
| build on PRs targeting `develop` or `master`, and on pushes to `develop`. Failed runs upload | ||
| `tmp/playwright/test-results` as an artifact retained for seven days. | ||
|
|
||
| ## Adding tests | ||
|
|
||
| Tag smoke tests with `{ tag: '@smoke' }`; run only those with `npm run test:e2e -- --grep @smoke`. | ||
|
|
||
| Add Playwright tests as `*.spec.ts` files in this directory. | ||
| Protractor `*.e2e-spec.ts` files are excluded from Playwright runs. | ||
|
|
||
| For tests that do not check font rendering, stub remote font stylesheet requests | ||
| with an empty CSS response so external font-service availability cannot affect | ||
| the result. See `layer.spec.ts` for an example. | ||
|
|
||
| ## Docker | ||
|
|
||
| Run from the repository root: | ||
|
|
||
| ```sh | ||
| docker build -f nav-app/e2e/Dockerfile -t attack-navigator-e2e:local . | ||
| docker run --rm --init attack-navigator-e2e:local | ||
| ``` | ||
|
|
||
| The image installs dependencies and Chromium, builds Navigator, and runs the same | ||
| suite. Source files are copied into the image; no host directories | ||
| are mounted. The local build and `node_modules` are not used. Test artifacts are | ||
| inside the container and are discarded by `--rm`. | ||
|
|
||
| Optional troubleshooting flags: | ||
|
|
||
| - Add `--platform linux/amd64` to both commands to match the GitHub runner's CPU | ||
| architecture when investigating differences between local and CI runs. | ||
| - Add `--shm-size=1g` to `docker run` to increase shared memory if Chromium crashes | ||
| because the container's default shared memory is insufficient. | ||
|
|
||
| If browser downloads fail with `SELF_SIGNED_CERT_IN_CHAIN`, set `CA_BUNDLE` to a | ||
| trusted PEM certificate bundle and supply it when building: | ||
|
|
||
| ```sh | ||
| docker build --secret "id=extra_ca,src=$CA_BUNDLE" -f nav-app/e2e/Dockerfile -t attack-navigator-e2e:local . | ||
| ``` | ||
|
|
||
| The optional build secret sets `NODE_EXTRA_CA_CERTS` only for browser installation. | ||
| The bundle is not stored in the image, and TLS verification is enabled. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| import { test, expect } from '@playwright/test'; | ||
| import { readFile } from 'node:fs/promises'; | ||
| import config from '../src/assets/config.json'; | ||
| import { configData, matrixSDO, TA0000, T0001, T0003 } from '../src/tests/utils/mock-data'; | ||
|
|
||
| test('create, score, rename, and export an Enterprise layer', { tag: '@smoke' }, async ({ page }, testInfo) => { | ||
| const errors: string[] = []; | ||
| page.on('pageerror', error => errors.push(error.message)); | ||
|
|
||
| // Reuse existing synthetic STIX fixtures; only data responses are replaced. | ||
| await page.route('**/assets/config.json', route => route.fulfill({ | ||
| json: { ...config, collection_index_url: '', versions: configData }, | ||
| })); | ||
| await page.route(configData.entries[0].domains[0].data[0], route => route.fulfill({ | ||
| json: { | ||
| type: 'bundle', | ||
| id: 'bundle-smoke', | ||
| objects: [matrixSDO, TA0000, { ...T0001, name: 'Smoke technique' }, { ...T0003, name: 'Untouched technique' }], | ||
| }, | ||
| })); | ||
|
Comment on lines
+10
to
+20
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks good, but to me it is highlighting that our mock data that it uses here hasn't been updated in 2+ years and probably longer than that, since https://github.com/mitre-attack/attack-navigator/blob/master/nav-app/src/tests/utils/mock-data.ts has ATT&CK v13 in it which is from April 2023. |
||
| // Avoid an external font dependency; assertions use text and attributes, not icon appearance. | ||
| await page.route('https://fonts.googleapis.com/**', route => route.fulfill({ body: '', contentType: 'text/css' })); | ||
|
|
||
| await page.goto('/'); | ||
| await page.getByText('Create New Layer', { exact: true }).click(); | ||
| await page.getByRole('button', { name: 'Enterprise', exact: true }).click(); | ||
| await expect(page.locator('technique-cell')).toHaveCount(2); | ||
| await page.locator('technique-cell').getByText('Smoke technique', { exact: true }).click(); | ||
|
|
||
| await page.getByText('Technique Controls', { exact: true }).click(); | ||
| await page.locator('span[alt="score"]').click(); | ||
| await page.getByLabel('score', { exact: true }).fill('1'); | ||
|
|
||
| await page.getByText('Layer Controls', { exact: true }).click(); | ||
| await page.locator('span[alt="layer information"]').click(); | ||
| await page.getByLabel('Name', { exact: true }).fill('Smoke layer'); | ||
| await expect(page.locator('.tab-title.active')).toContainText('Smoke layer'); | ||
|
|
||
| await page.locator('span[alt="export"]').click(); | ||
| const downloadPromise = page.waitForEvent('download'); | ||
| await page.locator('span[alt="save layer"]').filter({ hasText: /^code$/ }).click(); | ||
| const download = await downloadPromise; | ||
| const file = testInfo.outputPath('layer.json'); | ||
| await download.saveAs(file); | ||
| const layer = JSON.parse(await readFile(file, 'utf8')); | ||
| expect(layer).toMatchObject({ | ||
| name: 'Smoke layer', | ||
| domain: 'enterprise-attack', | ||
| versions: { attack: '13', layer: '4.5', navigator: expect.any(String) }, | ||
| }); | ||
| expect(layer.techniques).toHaveLength(1); | ||
| expect(layer.techniques[0]).toMatchObject({ techniqueID: 'T0001', tactic: 'tactic-name', score: 1 }); | ||
| expect(errors).toEqual([]); | ||
| }); | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import { defineConfig } from '@playwright/test'; | ||
|
|
||
| export default defineConfig({ | ||
| testDir: './e2e', | ||
| testMatch: '**/*.spec.ts', | ||
| outputDir: './tmp/playwright/test-results', | ||
| workers: 1, | ||
| retries: 0, | ||
| use: { | ||
| browserName: 'chromium', | ||
| baseURL: 'http://127.0.0.1:4173', | ||
| trace: 'retain-on-failure', | ||
| }, | ||
| webServer: { | ||
| command: 'python3 -m http.server 4173 --bind 127.0.0.1 --directory dist/browser', | ||
| url: 'http://127.0.0.1:4173', | ||
| }, | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.