From ae0514e67ff902b6c79460c2817846fdee7f5788 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 16 Aug 2022 11:17:32 -0400 Subject: [PATCH 01/69] WIP: Add data independent tests --- app/msw-mock-api.ts | 9 +++ app/pages/__tests__/orgs.e2e.ts | 33 ++++++----- app/test/helpers.ts | 70 ++++++++++++++++++++++++ libs/api-mocks/msw/db.ts | 12 ++-- package.json | 1 + patches/storage-as-an-object+1.0.1.patch | 32 +++++++++++ yarn.lock | 5 ++ 7 files changed, 143 insertions(+), 19 deletions(-) create mode 100644 app/test/helpers.ts create mode 100644 patches/storage-as-an-object+1.0.1.patch diff --git a/app/msw-mock-api.ts b/app/msw-mock-api.ts index 1d23f93cee..140c3e86fd 100644 --- a/app/msw-mock-api.ts +++ b/app/msw-mock-api.ts @@ -1,3 +1,5 @@ +import { resetDb } from '@oxide/api-mocks' + function getChaos() { const chaos = parseFloat(process.env.CHAOS || '') return Number.isNaN(chaos) ? null : chaos @@ -36,6 +38,13 @@ export async function startMockAPI() { const { handlers } = await import('@oxide/api-mocks') const { setupWorker, rest, compose } = await import('msw') + const params = window.location.search.slice(1).split('&') + if (params.includes('clear')) { + console.log('resetting db...') + resetDb() + window.location.search = params.filter((p) => p !== 'clear').join('&') + } + // defined in here because it depends on the dynamic import const chaosInterceptor = rest.all('/api/*', (_req, res, ctx) => { if (shouldFail(chaos)) { diff --git a/app/pages/__tests__/orgs.e2e.ts b/app/pages/__tests__/orgs.e2e.ts index 287b518647..69f11f231f 100644 --- a/app/pages/__tests__/orgs.e2e.ts +++ b/app/pages/__tests__/orgs.e2e.ts @@ -1,17 +1,19 @@ import { test } from '@playwright/test' +import { createOrganization } from 'app/test/helpers' import { expectVisible } from 'app/util/e2e' -test('Orgs list and detail click work', async ({ page }) => { +test('Root to orgs redirect', async ({ page }) => { await page.goto('/') - await expectVisible(page, [ - // note substring matcher bc headers have icons that mess with accessible name - // TODO: maybe that's bad and we should fix it in the code - 'role=heading[name*="Organizations"]', - 'role=cell[name="maze-war"]', - ]) + await page.waitForURL('/orgs') + await expectVisible(page, ['role=heading[name="Organizations"]']) +}) - // create org form +test('Orgs list and detail click work', async ({ page }) => { + await page.goto('/orgs') + await expectVisible(page, ['role=heading[name="Organizations"]']) + + // verify create org form await page.click('role=link[name="New Organization"]') await expectVisible(page, [ 'role=heading[name*="Create organization"]', @@ -21,14 +23,15 @@ test('Orgs list and detail click work', async ({ page }) => { ]) await page.goBack() + await createOrganization(page, { + name: 'org-create-test', + description: 'used to test org creation', + }) + // org page (redirects to /org/org-name/projects) - await page.click('role=link[name="maze-war"]') + await page.click('role=link[name="org-create-test"]') await expectVisible(page, [ - 'role=heading[name*="Projects"]', - 'role=cell[name="mock-project"]', + 'role=heading[name="Projects"]', + 'role=heading[name="No projects"]', ]) - - // new project button works - await page.click('role=link[name="New Project"]') - await expectVisible(page, ['role=heading[name*="Create project"]']) }) diff --git a/app/test/helpers.ts b/app/test/helpers.ts new file mode 100644 index 0000000000..8148230082 --- /dev/null +++ b/app/test/helpers.ts @@ -0,0 +1,70 @@ +import type { Page, Response } from '@playwright/test' + +import type { OrganizationCreate, ProjectCreate, ProjectCreateParams } from '@oxide/api' + +type Ok = [T, null] +type Err = [null, E] +type Result = Ok | Err +type ReturnFn = () => Promise + +// imitate Rust's Result constructors +const Ok = (o: T): Ok => [o, null] +const Err = (err: E): Err => [null, err] + +interface ReqError { + code: number + msg: string +} + +const goto = async (page: Page, url: string): Promise> => { + const currentUrl = page.url() + const response = await page.goto(url) + if (!response) throw new Error(`No response recieved for request to ${url}`) + const status = response.status() + if (status < 200 || status > 299) { + return Err({ + msg: `Loading ${url} failed with a status code of ${status}`, + code: status, + }) + } + return Ok(() => page.goto(currentUrl)) +} + +// --- Organizations --------- + +export async function createOrganization(page: Page, body: OrganizationCreate) { + const [back, err] = await goto(page, '/orgs/new') + if (err) { + throw new Error(err.msg) + } + await page.fill('role=textbox[name="Name"]', body.name) + await page.fill('role=textbox[name="Description"]', body.description) + await page.click('role=button[name="Create organization"]') + await page.waitForNavigation() + await back() +} + +// --- Projects -------------- + +export async function createProject( + page: Page, + params: ProjectCreateParams, + body: ProjectCreate +) { + const [back, err] = await goto(page, `/orgs/${params.orgName}/projects/new`) + // If there's a 404, the org likely doesn't exist. Try to create it before erroring. + if (err && err.code === 404) { + await createOrganization(page, { name: params.orgName, description: '' }) + const [, err] = await goto(page, `/orgs/${params.orgName}/projects/new`) + if (err) { + throw new Error( + `Failed to create project, couldn't navigate to new project view:\n ${err.msg}` + ) + } + } else if (err) { + throw new Error(err.msg) + } + await page.fill('role=textbox[name="Name"]', body.name) + await page.fill('role=textbox[name="Description"]', body.description) + await back!() +} diff --git a/libs/api-mocks/msw/db.ts b/libs/api-mocks/msw/db.ts index 49786b8d5d..9aae67999e 100644 --- a/libs/api-mocks/msw/db.ts +++ b/libs/api-mocks/msw/db.ts @@ -1,4 +1,6 @@ import { response } from 'msw' +// @ts-ignore +import createStore from 'storage-as-an-object' import type { Merge } from 'type-fest' import * as mock from '@oxide/api-mocks' @@ -166,10 +168,12 @@ const initDb = { vpcSubnets: [mock.vpcSubnet], } -const clone = (o: unknown) => JSON.parse(JSON.stringify(o)) +export const db: typeof initDb = createStore('msw-db', { + initialValues: structuredClone(initDb), + store: window.localStorage, +}) export function resetDb() { - db = clone(initDb) + // @ts-ignore + db.clear() } - -export let db: typeof initDb = clone(initDb) diff --git a/package.json b/package.json index c4462a099e..85d4de2e2e 100644 --- a/package.json +++ b/package.json @@ -126,6 +126,7 @@ "postcss-pseudo-classes": "^0.2.1", "prettier": "2.7.0", "require-from-string": "^2.0.2", + "storage-as-an-object": "^1.0.1", "style-dictionary": "^3.1.1", "tailwindcss": "^3.1.6", "token-transformer": "^0.0.18", diff --git a/patches/storage-as-an-object+1.0.1.patch b/patches/storage-as-an-object+1.0.1.patch new file mode 100644 index 0000000000..b854766ed6 --- /dev/null +++ b/patches/storage-as-an-object+1.0.1.patch @@ -0,0 +1,32 @@ +diff --git a/node_modules/storage-as-an-object/storageobject.js b/node_modules/storage-as-an-object/storageobject.js +index bdc7b3c..d080c00 100644 +--- a/node_modules/storage-as-an-object/storageobject.js ++++ b/node_modules/storage-as-an-object/storageobject.js +@@ -22,7 +22,7 @@ const defaults = { + function StorageObject(store_key, options={}) { + + let cache = {}; +- const opt = Object.assign({}, options); ++ const opt = Object.assign(defaults, options); + let timer = null + + const proxify = (obj) => { +@@ -82,8 +82,7 @@ function StorageObject(store_key, options={}) { + } + + const clear = () => { +- for (var key in cache) delete cache[key]; +- Object.assign(cache, opt.initialValues); ++ cache = structuredClone(opt.initialValues) + proxify(cache); + write(); + } +@@ -94,7 +93,7 @@ function StorageObject(store_key, options={}) { + try { + Object.assign(cache, JSON.parse(opt.store[store_key])); + } catch { +- Object.assign(cache, opt.initialValues); ++ Object.assign(cache, structuredClone(opt.initialValues)); + } + cache = proxify(cache); + write(); diff --git a/yarn.lock b/yarn.lock index 633e9b2e24..a8ffb65229 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14110,6 +14110,11 @@ statuses@^2.0.0: resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== +storage-as-an-object@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/storage-as-an-object/-/storage-as-an-object-1.0.1.tgz#aaa68bef61cfed30a92423e15b4e840279a6bc03" + integrity sha512-K19025EcIlolmiLCnGLUiMbWwW8+tbaEhu4lL4FsRvwWADM6WNFGIhbK1pSMdExPWV53rvVKOEVfoHQ/Ui3fPw== + store2@^2.12.0: version "2.12.0" resolved "https://registry.npmjs.org/store2/-/store2-2.12.0.tgz" From 9305e5973ec721c88080c7e76cd279f5cc3f6ffc Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 16 Aug 2022 15:50:22 -0400 Subject: [PATCH 02/69] Cleanup helpers --- app/pages/__tests__/orgs.e2e.ts | 4 ++-- app/test/helpers.ts | 25 ++++++++++++++----------- package.json | 2 +- yarn.lock | 18 +++++++++--------- 4 files changed, 26 insertions(+), 23 deletions(-) diff --git a/app/pages/__tests__/orgs.e2e.ts b/app/pages/__tests__/orgs.e2e.ts index 69f11f231f..6aa0961b43 100644 --- a/app/pages/__tests__/orgs.e2e.ts +++ b/app/pages/__tests__/orgs.e2e.ts @@ -1,6 +1,6 @@ import { test } from '@playwright/test' -import { createOrganization } from 'app/test/helpers' +import { createOrg } from 'app/test/helpers' import { expectVisible } from 'app/util/e2e' test('Root to orgs redirect', async ({ page }) => { @@ -23,7 +23,7 @@ test('Orgs list and detail click work', async ({ page }) => { ]) await page.goBack() - await createOrganization(page, { + await createOrg(page, { name: 'org-create-test', description: 'used to test org creation', }) diff --git a/app/test/helpers.ts b/app/test/helpers.ts index 8148230082..c9ab81b7a2 100644 --- a/app/test/helpers.ts +++ b/app/test/helpers.ts @@ -30,9 +30,18 @@ const goto = async (page: Page, url: string): Promise return Ok(() => page.goto(currentUrl)) } +export async function checkIfExists(page: Page, path: string) { + const res = await page.request.get(path) + const status = res.status() + if (status >= 500) { + throw new Error(`${path} failed to load with a ${status} response code`) + } + return status >= 200 && status < 300 +} + // --- Organizations --------- -export async function createOrganization(page: Page, body: OrganizationCreate) { +export async function createOrg(page: Page, body: OrganizationCreate) { const [back, err] = await goto(page, '/orgs/new') if (err) { throw new Error(err.msg) @@ -51,17 +60,11 @@ export async function createProject( params: ProjectCreateParams, body: ProjectCreate ) { + if (!checkIfExists(page, `/orgs/${params.orgName}`)) { + await createOrg(page, { name: params.orgName, description: '' }) + } const [back, err] = await goto(page, `/orgs/${params.orgName}/projects/new`) - // If there's a 404, the org likely doesn't exist. Try to create it before erroring. - if (err && err.code === 404) { - await createOrganization(page, { name: params.orgName, description: '' }) - const [, err] = await goto(page, `/orgs/${params.orgName}/projects/new`) - if (err) { - throw new Error( - `Failed to create project, couldn't navigate to new project view:\n ${err.msg}` - ) - } - } else if (err) { + if (err) { throw new Error(err.msg) } await page.fill('role=textbox[name="Name"]', body.name) diff --git a/package.json b/package.json index 85d4de2e2e..882fa89010 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "@figma-export/cli": "^4.3.0", "@figma-export/output-components-as-svgr": "^4.2.0", "@figma-export/transform-svg-with-svgo": "^4.3.0", - "@playwright/test": "^1.23.4", + "@playwright/test": "^1.25.0", "@storybook/addon-docs": "^6.5.9", "@storybook/addon-essentials": "^6.5.9", "@storybook/addon-links": "^6.5.9", diff --git a/yarn.lock b/yarn.lock index a8ffb65229..d932fddcf3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2606,13 +2606,13 @@ resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-1.0.3.tgz#db9cc719191a62e7d9200f6e7bab21c5b848adca" integrity sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q== -"@playwright/test@^1.23.4": - version "1.23.4" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.23.4.tgz#d742ed34fc3eb09e29c2e652c2a646ba1e3c6b08" - integrity sha512-iIsoMJDS/lyuhw82FtcV/B3PXikgVD3hNe5hyvOpRM0uRr1OIpN3LgPeRbBjhzBWmyf6RgRg5fqK5sVcpA03yA== +"@playwright/test@^1.25.0": + version "1.25.0" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.25.0.tgz#e0de134651e78e45e986c5f16578188dd5937331" + integrity sha512-j4EZhTTQI3dBeWblE21EV//swwmBtOpIrLdOIJIRv4uqsLdHgBg1z+JtTg+AeC5o2bAXIE26kDNW5A0TimG8Bg== dependencies: "@types/node" "*" - playwright-core "1.23.4" + playwright-core "1.25.0" "@pmmmwh/react-refresh-webpack-plugin@^0.5.3": version "0.5.7" @@ -12256,10 +12256,10 @@ pkg-dir@^5.0.0: dependencies: find-up "^5.0.0" -playwright-core@1.23.4: - version "1.23.4" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.23.4.tgz#e8a45e549faf6bfad24a0e9998e451979514d41e" - integrity sha512-h5V2yw7d8xIwotjyNrkLF13nV9RiiZLHdXeHo+nVJIYGVlZ8U2qV0pMxNJKNTvfQVT0N8/A4CW6/4EW2cOcTiA== +playwright-core@1.25.0: + version "1.25.0" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.25.0.tgz#54dc867c6c2cc5e4233905e249206a02914d14f1" + integrity sha512-kZ3Jwaf3wlu0GgU0nB8UMQ+mXFTqBIFz9h1svTlNduNKjnbPXFxw7mJanLVjqxHJRn62uBfmgBj93YHidk2N5Q== plop@^2.7.6: version "2.7.6" From 919b57bd68209b3f3b38d349c7c6747581927a65 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 16 Aug 2022 16:13:26 -0400 Subject: [PATCH 03/69] Add removal handlers --- app/test/helpers.ts | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/app/test/helpers.ts b/app/test/helpers.ts index c9ab81b7a2..3efd7da497 100644 --- a/app/test/helpers.ts +++ b/app/test/helpers.ts @@ -1,6 +1,12 @@ import type { Page, Response } from '@playwright/test' -import type { OrganizationCreate, ProjectCreate, ProjectCreateParams } from '@oxide/api' +import type { + OrganizationCreate, + OrganizationDeleteParams, + ProjectCreate, + ProjectCreateParams, + ProjectDeleteParams, +} from '@oxide/api' type Ok = [T, null] type Err = [null, E] @@ -41,6 +47,10 @@ export async function checkIfExists(page: Page, path: string) { // --- Organizations --------- +/** + * Creates an Organization and returns to the page it was called from. Returns + * a cleanup function that can be used to cleanup the org later. + */ export async function createOrg(page: Page, body: OrganizationCreate) { const [back, err] = await goto(page, '/orgs/new') if (err) { @@ -51,17 +61,33 @@ export async function createOrg(page: Page, body: OrganizationCreate) { await page.click('role=button[name="Create organization"]') await page.waitForNavigation() await back() + return () => deleteOrg(page, { orgName: body.name }) +} + +export async function deleteOrg(page: Page, params: OrganizationDeleteParams) { + const res = await page.request.delete(`/orgs/${params.orgName}`) + const status = res.status() + if (status >= 300) { + throw new Error(`Failed to delete org ${params.orgName} with response code ${status}`) + } } // --- Projects -------------- +/* + * Creates a project (and its parent org if it doesn't already exist) then returns + * to the page it was called from. Returns a function that will delete the project + * (and org if one was created). + */ export async function createProject( page: Page, params: ProjectCreateParams, body: ProjectCreate ) { + let orgCreated = false if (!checkIfExists(page, `/orgs/${params.orgName}`)) { await createOrg(page, { name: params.orgName, description: '' }) + orgCreated = true } const [back, err] = await goto(page, `/orgs/${params.orgName}/projects/new`) if (err) { @@ -70,4 +96,19 @@ export async function createProject( await page.fill('role=textbox[name="Name"]', body.name) await page.fill('role=textbox[name="Description"]', body.description) await back!() + + return async () => { + await deleteProject(page, { ...params, projectName: body.name }) + if (orgCreated) { + await deleteOrg(page, params) + } + } +} + +export async function deleteProject(page: Page, params: ProjectDeleteParams) { + const res = await page.request.delete(`/orgs/${params.orgName}`) + const status = res.status() + if (status >= 300) { + throw new Error(`Failed to delete org ${params.orgName} with response code ${status}`) + } } From 9f06a79e24241b967903ec7224da82202e123f1e Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 17 Aug 2022 13:46:58 -0400 Subject: [PATCH 04/69] Make cleanup implicit --- app/test/helpers.ts | 23 ++++++++++++++++------- app/test/setup.ts | 4 +++- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/app/test/helpers.ts b/app/test/helpers.ts index 3efd7da497..124a23541e 100644 --- a/app/test/helpers.ts +++ b/app/test/helpers.ts @@ -22,6 +22,17 @@ interface ReqError { msg: string } +const cleanupTasks: Array<() => Promise> = [] + +/** + * Cleans any resources that might've been left around after a test + */ +export const cleanup = async () => { + for (const task of cleanupTasks) { + await task() + } +} + const goto = async (page: Page, url: string): Promise> => { const currentUrl = page.url() const response = await page.goto(url) @@ -48,8 +59,7 @@ export async function checkIfExists(page: Page, path: string) { // --- Organizations --------- /** - * Creates an Organization and returns to the page it was called from. Returns - * a cleanup function that can be used to cleanup the org later. + * Creates an Organization and returns to the page it was called from. */ export async function createOrg(page: Page, body: OrganizationCreate) { const [back, err] = await goto(page, '/orgs/new') @@ -61,7 +71,7 @@ export async function createOrg(page: Page, body: OrganizationCreate) { await page.click('role=button[name="Create organization"]') await page.waitForNavigation() await back() - return () => deleteOrg(page, { orgName: body.name }) + cleanupTasks.push(() => deleteOrg(page, { orgName: body.name })) } export async function deleteOrg(page: Page, params: OrganizationDeleteParams) { @@ -76,8 +86,7 @@ export async function deleteOrg(page: Page, params: OrganizationDeleteParams) { /* * Creates a project (and its parent org if it doesn't already exist) then returns - * to the page it was called from. Returns a function that will delete the project - * (and org if one was created). + * to the page it was called from. */ export async function createProject( page: Page, @@ -97,12 +106,12 @@ export async function createProject( await page.fill('role=textbox[name="Description"]', body.description) await back!() - return async () => { + cleanupTasks.push(async () => { await deleteProject(page, { ...params, projectName: body.name }) if (orgCreated) { await deleteOrg(page, params) } - } + }) } export async function deleteProject(page: Page, params: ProjectDeleteParams) { diff --git a/app/test/setup.ts b/app/test/setup.ts index 7af716a641..f42d315499 100644 --- a/app/test/setup.ts +++ b/app/test/setup.ts @@ -5,10 +5,12 @@ import 'whatwg-fetch' // fancy asserts import { resetDb } from '@oxide/api-mocks' +import { cleanup } from './helpers' import { server } from './server' beforeAll(() => server.listen()) -afterEach(() => { +afterEach(async () => { + await cleanup() resetDb() server.resetHandlers() }) From 80c51f77c2117f13aa3891225c286562550b7c75 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 17 Aug 2022 16:04:19 -0400 Subject: [PATCH 05/69] Fork off simplified version of store proxy --- libs/api-mocks/msw/db.ts | 8 +- libs/api-mocks/msw/store.ts | 109 +++++++++++++++++++++++ package.json | 1 - patches/storage-as-an-object+1.0.1.patch | 32 ------- yarn.lock | 5 -- 5 files changed, 112 insertions(+), 43 deletions(-) create mode 100644 libs/api-mocks/msw/store.ts delete mode 100644 patches/storage-as-an-object+1.0.1.patch diff --git a/libs/api-mocks/msw/db.ts b/libs/api-mocks/msw/db.ts index 9aae67999e..ced6c76cf6 100644 --- a/libs/api-mocks/msw/db.ts +++ b/libs/api-mocks/msw/db.ts @@ -1,6 +1,5 @@ import { response } from 'msw' // @ts-ignore -import createStore from 'storage-as-an-object' import type { Merge } from 'type-fest' import * as mock from '@oxide/api-mocks' @@ -8,6 +7,7 @@ import type { ApiTypes as Api } from '@oxide/api' import { sessionMe } from '@oxide/api-mocks' import type { Json } from '../json-type' +import { createStore } from './store' import { json } from './util' const notFoundBody = { error_code: 'ObjectNotFound' } as const @@ -168,12 +168,10 @@ const initDb = { vpcSubnets: [mock.vpcSubnet], } -export const db: typeof initDb = createStore('msw-db', { - initialValues: structuredClone(initDb), - store: window.localStorage, +export const db = createStore('msw-db', { + initialValues: structuredClone(initDb) as typeof initDb, }) export function resetDb() { - // @ts-ignore db.clear() } diff --git a/libs/api-mocks/msw/store.ts b/libs/api-mocks/msw/store.ts new file mode 100644 index 0000000000..8bd282da88 --- /dev/null +++ b/libs/api-mocks/msw/store.ts @@ -0,0 +1,109 @@ +/** + * This is a fork of https://github.com/ropg/storage-as-an-object + * storage-as-an-object is MIT license which can be found here: + * https://github.com/ropg/storage-as-an-object/blob/7a12a1dd3bcc1b87a77e8202fae98ae25323b968/LICENSE + */ + +interface StoreOptions> { + initialValues: T + store?: Storage + debounceTime?: number +} + +/** + * createStore Creates an object that's persisted to a storage target like + * localStorage or sessionStorage + * + * @param {string} key - The name of the string in the store + * @param {StoreOptions} [options] - Optional configuration + * @returns {Object} - The object that your code interfaces with + */ +export function createStore>( + key: string, + options: StoreOptions +) { + const debounceTime = options.debounceTime ?? 100 + let cache = {} as T + let timer: NodeJS.Timeout | null = null + const store = options.store ?? window.localStorage + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const proxify = (obj: any) => { + const validTypes = ['string', 'number', 'object', 'undefined', 'boolean'] + if (!validTypes.includes(typeof obj)) { + throw new Error('StorageObject does not store this variable type') + } + if (typeof obj === 'object' && obj !== null) { + if (obj instanceof Date) { + return { __DATE: obj.toJSON() } + } + for (const key in obj) { + obj[key] = proxify(obj[key]) + } + return new Proxy(obj, { + get(target, name) { + const val = target[name] + if (val && typeof val === 'object' && val.__DATE) { + return new Date(val.__DATE) + } + return val + }, + set(target, name, value) { + target[name] = proxify(value) + debounceWrite() + return true + }, + deleteProperty(target, name) { + delete target[name] + debounceWrite() + return true + }, + }) + } + return obj + } + + const write = () => { + if (timer) { + clearTimeout(timer) + timer = null + } + store[key] = JSON.stringify(cache) + } + + const debounceWrite = () => { + if (debounceTime === 0) { + write() + return + } + if (timer) { + clearTimeout(timer) + } + timer = setTimeout(write, debounceTime) + } + + const clear = () => { + cache = structuredClone(options.initialValues) + proxify(cache) + write() + } + Object.defineProperty(cache, 'clear', { value: clear }) + + const unload = () => { + if (timer) write() + } + + try { + Object.assign(cache, JSON.parse(store[key])) + } catch { + Object.assign(cache, structuredClone(options.initialValues)) + } + cache = proxify(cache) + write() + + if (debounceTime > 0) { + window.addEventListener('unload', unload) + } + + return cache as T & { clear: () => void } +} diff --git a/package.json b/package.json index 882fa89010..9c9948ab36 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,6 @@ "postcss-pseudo-classes": "^0.2.1", "prettier": "2.7.0", "require-from-string": "^2.0.2", - "storage-as-an-object": "^1.0.1", "style-dictionary": "^3.1.1", "tailwindcss": "^3.1.6", "token-transformer": "^0.0.18", diff --git a/patches/storage-as-an-object+1.0.1.patch b/patches/storage-as-an-object+1.0.1.patch deleted file mode 100644 index b854766ed6..0000000000 --- a/patches/storage-as-an-object+1.0.1.patch +++ /dev/null @@ -1,32 +0,0 @@ -diff --git a/node_modules/storage-as-an-object/storageobject.js b/node_modules/storage-as-an-object/storageobject.js -index bdc7b3c..d080c00 100644 ---- a/node_modules/storage-as-an-object/storageobject.js -+++ b/node_modules/storage-as-an-object/storageobject.js -@@ -22,7 +22,7 @@ const defaults = { - function StorageObject(store_key, options={}) { - - let cache = {}; -- const opt = Object.assign({}, options); -+ const opt = Object.assign(defaults, options); - let timer = null - - const proxify = (obj) => { -@@ -82,8 +82,7 @@ function StorageObject(store_key, options={}) { - } - - const clear = () => { -- for (var key in cache) delete cache[key]; -- Object.assign(cache, opt.initialValues); -+ cache = structuredClone(opt.initialValues) - proxify(cache); - write(); - } -@@ -94,7 +93,7 @@ function StorageObject(store_key, options={}) { - try { - Object.assign(cache, JSON.parse(opt.store[store_key])); - } catch { -- Object.assign(cache, opt.initialValues); -+ Object.assign(cache, structuredClone(opt.initialValues)); - } - cache = proxify(cache); - write(); diff --git a/yarn.lock b/yarn.lock index d932fddcf3..ddc7da012c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14110,11 +14110,6 @@ statuses@^2.0.0: resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== -storage-as-an-object@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/storage-as-an-object/-/storage-as-an-object-1.0.1.tgz#aaa68bef61cfed30a92423e15b4e840279a6bc03" - integrity sha512-K19025EcIlolmiLCnGLUiMbWwW8+tbaEhu4lL4FsRvwWADM6WNFGIhbK1pSMdExPWV53rvVKOEVfoHQ/Ui3fPw== - store2@^2.12.0: version "2.12.0" resolved "https://registry.npmjs.org/store2/-/store2-2.12.0.tgz" From 834ac422775008e63464872a50d75e1f57bfa250 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 17 Aug 2022 16:11:43 -0400 Subject: [PATCH 06/69] Remove stray ts-ignore --- libs/api-mocks/msw/db.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/api-mocks/msw/db.ts b/libs/api-mocks/msw/db.ts index ced6c76cf6..b0f4105a52 100644 --- a/libs/api-mocks/msw/db.ts +++ b/libs/api-mocks/msw/db.ts @@ -1,5 +1,4 @@ import { response } from 'msw' -// @ts-ignore import type { Merge } from 'type-fest' import * as mock from '@oxide/api-mocks' From 12b2c74ef7377e59b59ff8dbe6d79631ba3caf67 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 17 Aug 2022 16:58:09 -0400 Subject: [PATCH 07/69] Fix tests --- libs/api-mocks/msw/db.ts | 17 ++++++++++++----- libs/api-mocks/msw/store.ts | 5 +++-- libs/api-mocks/msw/util.ts | 5 +++++ libs/api/__tests__/safety.spec.ts | 2 +- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/libs/api-mocks/msw/db.ts b/libs/api-mocks/msw/db.ts index b0f4105a52..4aa0f084dd 100644 --- a/libs/api-mocks/msw/db.ts +++ b/libs/api-mocks/msw/db.ts @@ -7,7 +7,7 @@ import { sessionMe } from '@oxide/api-mocks' import type { Json } from '../json-type' import { createStore } from './store' -import { json } from './util' +import { clone, json } from './util' const notFoundBody = { error_code: 'ObjectNotFound' } as const export type NotFound = typeof notFoundBody @@ -167,10 +167,17 @@ const initDb = { vpcSubnets: [mock.vpcSubnet], } -export const db = createStore('msw-db', { - initialValues: structuredClone(initDb) as typeof initDb, -}) +export let db: typeof initDb & { clear?: () => void } = + typeof window !== 'undefined' + ? createStore('msw-db', { + initialValues: clone(initDb), + }) + : initDb export function resetDb() { - db.clear() + if (db.clear) { + db.clear() + } else { + db = clone(db) + } } diff --git a/libs/api-mocks/msw/store.ts b/libs/api-mocks/msw/store.ts index 8bd282da88..5b9d9a9410 100644 --- a/libs/api-mocks/msw/store.ts +++ b/libs/api-mocks/msw/store.ts @@ -3,6 +3,7 @@ * storage-as-an-object is MIT license which can be found here: * https://github.com/ropg/storage-as-an-object/blob/7a12a1dd3bcc1b87a77e8202fae98ae25323b968/LICENSE */ +import { clone } from './util' interface StoreOptions> { initialValues: T @@ -83,7 +84,7 @@ export function createStore>( } const clear = () => { - cache = structuredClone(options.initialValues) + cache = clone(options.initialValues) proxify(cache) write() } @@ -96,7 +97,7 @@ export function createStore>( try { Object.assign(cache, JSON.parse(store[key])) } catch { - Object.assign(cache, structuredClone(options.initialValues)) + Object.assign(cache, clone(options.initialValues)) } cache = proxify(cache) write() diff --git a/libs/api-mocks/msw/util.ts b/libs/api-mocks/msw/util.ts index ddbf792612..fddbfb01a1 100644 --- a/libs/api-mocks/msw/util.ts +++ b/libs/api-mocks/msw/util.ts @@ -55,3 +55,8 @@ export const paginated = ( // testing pagination export const repeat = (obj: T, n: number): T[] => new Array(n).fill(0).map((_, i) => ({ ...obj, id: obj.id + i, name: obj.name + i })) + +export const clone = (obj: T): T => + typeof structuredClone !== 'undefined' + ? structuredClone(obj) + : JSON.parse(JSON.stringify(obj)) diff --git a/libs/api/__tests__/safety.spec.ts b/libs/api/__tests__/safety.spec.ts index 303ad9e2af..ee68190e97 100644 --- a/libs/api/__tests__/safety.spec.ts +++ b/libs/api/__tests__/safety.spec.ts @@ -27,7 +27,7 @@ const grepFiles = (s: string) => it('@oxide/api-mocks is only referenced in test files', () => { const files = grepFiles("from '@oxide/api-mocks'") for (const file of files) { - expect(file).toMatch(/__tests__\/|app\/test\/|\.spec\.|tsconfig|api-mocks/) + expect(file).toMatch(/__tests__\/|app\/test\/|\.spec\.|tsconfig|api-mocks|msw-mock-api/) } }) From f6b18825bc17f8767db80acd8f89cfdc47ab3fb4 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 17 Aug 2022 17:51:59 -0400 Subject: [PATCH 08/69] Restore manual cleanup --- app/pages/__tests__/orgs.e2e.ts | 4 +++- app/test/helpers.ts | 17 +++-------------- app/test/setup.ts | 6 ++++-- 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/app/pages/__tests__/orgs.e2e.ts b/app/pages/__tests__/orgs.e2e.ts index 6aa0961b43..1942a6d287 100644 --- a/app/pages/__tests__/orgs.e2e.ts +++ b/app/pages/__tests__/orgs.e2e.ts @@ -23,7 +23,7 @@ test('Orgs list and detail click work', async ({ page }) => { ]) await page.goBack() - await createOrg(page, { + const removeOrg = await createOrg(page, { name: 'org-create-test', description: 'used to test org creation', }) @@ -34,4 +34,6 @@ test('Orgs list and detail click work', async ({ page }) => { 'role=heading[name="Projects"]', 'role=heading[name="No projects"]', ]) + + await removeOrg() }) diff --git a/app/test/helpers.ts b/app/test/helpers.ts index 124a23541e..4b313a4270 100644 --- a/app/test/helpers.ts +++ b/app/test/helpers.ts @@ -22,17 +22,6 @@ interface ReqError { msg: string } -const cleanupTasks: Array<() => Promise> = [] - -/** - * Cleans any resources that might've been left around after a test - */ -export const cleanup = async () => { - for (const task of cleanupTasks) { - await task() - } -} - const goto = async (page: Page, url: string): Promise> => { const currentUrl = page.url() const response = await page.goto(url) @@ -71,7 +60,7 @@ export async function createOrg(page: Page, body: OrganizationCreate) { await page.click('role=button[name="Create organization"]') await page.waitForNavigation() await back() - cleanupTasks.push(() => deleteOrg(page, { orgName: body.name })) + return () => deleteOrg(page, { orgName: body.name }) } export async function deleteOrg(page: Page, params: OrganizationDeleteParams) { @@ -106,12 +95,12 @@ export async function createProject( await page.fill('role=textbox[name="Description"]', body.description) await back!() - cleanupTasks.push(async () => { + return async () => { await deleteProject(page, { ...params, projectName: body.name }) if (orgCreated) { await deleteOrg(page, params) } - }) + } } export async function deleteProject(page: Page, params: ProjectDeleteParams) { diff --git a/app/test/setup.ts b/app/test/setup.ts index f42d315499..1b0c2030fa 100644 --- a/app/test/setup.ts +++ b/app/test/setup.ts @@ -1,3 +1,7 @@ +/** + * This file is ran by vitest before any tests are ran. Configuration + * in this file does _not_ impact end-to-end tests. + */ // our node fetch polyfill of choice import '@testing-library/jest-dom' import 'whatwg-fetch' @@ -5,12 +9,10 @@ import 'whatwg-fetch' // fancy asserts import { resetDb } from '@oxide/api-mocks' -import { cleanup } from './helpers' import { server } from './server' beforeAll(() => server.listen()) afterEach(async () => { - await cleanup() resetDb() server.resetHandlers() }) From 619bf6e69941c4312ac4f92fe6a2eb3479ae02a2 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 17 Aug 2022 18:39:07 -0400 Subject: [PATCH 09/69] Flesh out deletes --- app/pages/__tests__/orgs.e2e.ts | 2 +- app/test/helpers.ts | 34 +++++++++++++++++++++++++-------- libs/api-mocks/msw/handlers.ts | 14 +++++++------- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/app/pages/__tests__/orgs.e2e.ts b/app/pages/__tests__/orgs.e2e.ts index 1942a6d287..e4b53cfff8 100644 --- a/app/pages/__tests__/orgs.e2e.ts +++ b/app/pages/__tests__/orgs.e2e.ts @@ -9,7 +9,7 @@ test('Root to orgs redirect', async ({ page }) => { await expectVisible(page, ['role=heading[name="Organizations"]']) }) -test('Orgs list and detail click work', async ({ page }) => { +test('Create org and navigate to project', async ({ page }) => { await page.goto('/orgs') await expectVisible(page, ['role=heading[name="Organizations"]']) diff --git a/app/test/helpers.ts b/app/test/helpers.ts index 4b313a4270..991a90da55 100644 --- a/app/test/helpers.ts +++ b/app/test/helpers.ts @@ -8,6 +8,8 @@ import type { ProjectDeleteParams, } from '@oxide/api' +import { expectNotVisible } from 'app/util/e2e' + type Ok = [T, null] type Err = [null, E] type Result = Ok | Err @@ -64,11 +66,19 @@ export async function createOrg(page: Page, body: OrganizationCreate) { } export async function deleteOrg(page: Page, params: OrganizationDeleteParams) { - const res = await page.request.delete(`/orgs/${params.orgName}`) - const status = res.status() - if (status >= 300) { - throw new Error(`Failed to delete org ${params.orgName} with response code ${status}`) + const [back, err] = await goto(page, '/orgs') + if (err) { + throw new Error(err.msg) } + + await page + .locator('role=row', { hasText: params.orgName }) + .locator('role=button[name="Row actions"]') + .click() + await page.click('role=menuitem[name="Delete"]') + await expectNotVisible(page, [`role=cell[name="${params.orgName}"]`]) + + await back() } // --- Projects -------------- @@ -104,9 +114,17 @@ export async function createProject( } export async function deleteProject(page: Page, params: ProjectDeleteParams) { - const res = await page.request.delete(`/orgs/${params.orgName}`) - const status = res.status() - if (status >= 300) { - throw new Error(`Failed to delete org ${params.orgName} with response code ${status}`) + const [back, err] = await goto(page, `/orgs/${params.orgName}/projects`) + if (err) { + throw new Error(err.msg) } + + await page + .locator('role=row', { hasText: params.projectName }) + .locator('role=button[name="Row actions"]') + .click() + await page.click('role=menuitem[name="Delete"]') + await expectNotVisible(page, [`role=cell[name="${params.projectName}"]`]) + + await back() } diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index f52ca0b509..467930addf 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -184,6 +184,13 @@ export const handlers = [ } ), + rest.delete('/api/organizations/:orgName', (req, res, ctx) => { + const [org, err] = lookupOrg(req.params) + if (err) return res(err) + db.orgs = db.orgs.filter((o) => o.id !== org.id) + return res(ctx.status(204)) + }), + rest.get | GetErr>( '/api/organizations/:orgName/policy', (req, res) => { @@ -221,13 +228,6 @@ export const handlers = [ return res(json(req.body)) }), - rest.delete('/api/organizations/:orgName', (req, res, ctx) => { - const [org, err] = lookupOrg(req.params) - if (err) return res(err) - db.orgs = db.orgs.filter((o) => o.id !== org.id) - return res(ctx.status(204)) - }), - rest.get | GetErr>( '/api/organizations/:orgName/projects', (req, res) => { From f9eb04a16a5bab948d63ea8509e3ac013b4c3ed8 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 17 Aug 2022 19:21:32 -0400 Subject: [PATCH 10/69] Revert handler changes --- libs/api-mocks/msw/handlers.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index 0f4438166c..9d97ad7438 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -232,6 +232,13 @@ export const handlers = [ return res(json(req.body)) }), + rest.delete('/api/organizations/:orgName', (req, res, ctx) => { + const [org, err] = lookupOrg(req.params) + if (err) return res(err) + db.orgs = db.orgs.filter((o) => o.id !== org.id) + return res(ctx.status(204)) + }), + rest.get | GetErr>( '/api/organizations/:orgName/projects', (req, res) => { From 0329fcbf8ded102abbe910518bd19f9881a7b935 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 17 Aug 2022 19:26:29 -0400 Subject: [PATCH 11/69] Make org tests work in parallel --- app/pages/__tests__/orgs.e2e.ts | 7 ++++--- app/test/helpers.ts | 21 +++++++++++++++++++-- libs/api-mocks/msw/db.ts | 15 ++++----------- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/app/pages/__tests__/orgs.e2e.ts b/app/pages/__tests__/orgs.e2e.ts index e4b53cfff8..7623c12515 100644 --- a/app/pages/__tests__/orgs.e2e.ts +++ b/app/pages/__tests__/orgs.e2e.ts @@ -1,6 +1,6 @@ import { test } from '@playwright/test' -import { createOrg } from 'app/test/helpers' +import { createOrg, genName } from 'app/test/helpers' import { expectVisible } from 'app/util/e2e' test('Root to orgs redirect', async ({ page }) => { @@ -23,13 +23,14 @@ test('Create org and navigate to project', async ({ page }) => { ]) await page.goBack() + const orgName = genName('org-create-test') const removeOrg = await createOrg(page, { - name: 'org-create-test', + name: orgName, description: 'used to test org creation', }) // org page (redirects to /org/org-name/projects) - await page.click('role=link[name="org-create-test"]') + await page.click(`role=link[name="${orgName}"]`) await expectVisible(page, [ 'role=heading[name="Projects"]', 'role=heading[name="No projects"]', diff --git a/app/test/helpers.ts b/app/test/helpers.ts index 991a90da55..443460fe07 100644 --- a/app/test/helpers.ts +++ b/app/test/helpers.ts @@ -24,12 +24,27 @@ interface ReqError { msg: string } +// TODO: This is duplicated from `@oxide/api` and can't be pulled in due to +// inlined tests using `import.meta` being imported and causing a syntax error +// given that playwright converts all the tests to commonjs. +export const genName = (...parts: [string, ...string[]]) => { + const numParts = parts.length + const partLength = Math.floor(63 / numParts) - Math.ceil(6 / numParts) - 1 + return ( + parts + .map((part) => part.substring(0, partLength)) + .join('-') + // generate random hex string of 6 characters + .concat(`-${Math.random().toString(16).substring(2, 8)}`) + ) +} + const goto = async (page: Page, url: string): Promise> => { const currentUrl = page.url() const response = await page.goto(url) if (!response) throw new Error(`No response recieved for request to ${url}`) const status = response.status() - if (status < 200 || status > 299) { + if (status > 399) { return Err({ msg: `Loading ${url} failed with a status code of ${status}`, code: status, @@ -62,7 +77,9 @@ export async function createOrg(page: Page, body: OrganizationCreate) { await page.click('role=button[name="Create organization"]') await page.waitForNavigation() await back() - return () => deleteOrg(page, { orgName: body.name }) + return () => { + return deleteOrg(page, { orgName: body.name }) + } } export async function deleteOrg(page: Page, params: OrganizationDeleteParams) { diff --git a/libs/api-mocks/msw/db.ts b/libs/api-mocks/msw/db.ts index 4aa0f084dd..2b4e5cdbc7 100644 --- a/libs/api-mocks/msw/db.ts +++ b/libs/api-mocks/msw/db.ts @@ -167,17 +167,10 @@ const initDb = { vpcSubnets: [mock.vpcSubnet], } -export let db: typeof initDb & { clear?: () => void } = - typeof window !== 'undefined' - ? createStore('msw-db', { - initialValues: clone(initDb), - }) - : initDb +export const db = createStore('msw-db', { + initialValues: clone(initDb), +}) export function resetDb() { - if (db.clear) { - db.clear() - } else { - db = clone(db) - } + db.clear() } From 01ea89364cb9f9369614769beb0965697dabd2c2 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 18 Aug 2022 01:53:41 -0400 Subject: [PATCH 12/69] e2e fixture experimentation --- app/forms/__tests__/instance-create.e2e.ts | 4 +- app/pages/__tests__/NotFound.e2e.ts | 2 +- app/pages/__tests__/click-everything.e2e.ts | 4 +- .../__tests__/instance/attach-disk.e2e.ts | 4 +- .../__tests__/instance/networking.e2e.ts | 10 +- app/pages/__tests__/org-access.e2e.ts | 4 +- app/pages/__tests__/orgs.e2e.ts | 16 +- app/pages/__tests__/project-access.e2e.ts | 4 +- app/pages/__tests__/project-create.e2e.tsx | 4 +- app/pages/__tests__/project-selector.e2e.ts | 4 +- app/pages/__tests__/row-select.e2e.ts | 4 +- app/pages/__tests__/ssh-keys.e2e.ts | 4 +- app/test/helpers.ts | 147 ------------------ libs/test/fixtures.ts | 93 +++++++++++ libs/test/index.ts | 55 +++++++ app/util/e2e.ts => libs/test/utils.ts | 15 ++ tsconfig.json | 1 + 17 files changed, 186 insertions(+), 189 deletions(-) delete mode 100644 app/test/helpers.ts create mode 100644 libs/test/fixtures.ts create mode 100644 libs/test/index.ts rename app/util/e2e.ts => libs/test/utils.ts (79%) diff --git a/app/forms/__tests__/instance-create.e2e.ts b/app/forms/__tests__/instance-create.e2e.ts index 6fbc45ba0f..2c52e5f7a2 100644 --- a/app/forms/__tests__/instance-create.e2e.ts +++ b/app/forms/__tests__/instance-create.e2e.ts @@ -1,6 +1,4 @@ -import { expect, test } from '@playwright/test' - -import { expectVisible } from 'app/util/e2e' +import { expect, expectVisible, test } from '@oxide/test' test.describe('Instance Create Form', () => { test('can invoke instance create form from instances page', async ({ page }) => { diff --git a/app/pages/__tests__/NotFound.e2e.ts b/app/pages/__tests__/NotFound.e2e.ts index a02ca9eadc..f3d247654c 100644 --- a/app/pages/__tests__/NotFound.e2e.ts +++ b/app/pages/__tests__/NotFound.e2e.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@playwright/test' +import { expect, test } from '@oxide/test' test('Shows 404 page when a resource is not found', async ({ page }) => { await page.goto('/orgs/nonexistent') diff --git a/app/pages/__tests__/click-everything.e2e.ts b/app/pages/__tests__/click-everything.e2e.ts index 22b0027b2c..043f85ad98 100644 --- a/app/pages/__tests__/click-everything.e2e.ts +++ b/app/pages/__tests__/click-everything.e2e.ts @@ -1,6 +1,4 @@ -import { test } from '@playwright/test' - -import { expectNotVisible, expectVisible } from 'app/util/e2e' +import { expectNotVisible, expectVisible, test } from '@oxide/test' test("Click through everything and make it's all there", async ({ page }) => { await page.goto('/orgs/maze-war/projects') diff --git a/app/pages/__tests__/instance/attach-disk.e2e.ts b/app/pages/__tests__/instance/attach-disk.e2e.ts index 59a172969a..6fd57e318d 100644 --- a/app/pages/__tests__/instance/attach-disk.e2e.ts +++ b/app/pages/__tests__/instance/attach-disk.e2e.ts @@ -1,6 +1,4 @@ -import { test } from '@playwright/test' - -import { expectVisible } from 'app/util/e2e' +import { expectVisible, test } from '@oxide/test' import { stopInstance } from './util' diff --git a/app/pages/__tests__/instance/networking.e2e.ts b/app/pages/__tests__/instance/networking.e2e.ts index ea2d14877b..39d6703544 100644 --- a/app/pages/__tests__/instance/networking.e2e.ts +++ b/app/pages/__tests__/instance/networking.e2e.ts @@ -1,6 +1,10 @@ -import { expect, test } from '@playwright/test' - -import { expectNotVisible, expectRowVisible, expectVisible } from 'app/util/e2e' +import { + expect, + expectNotVisible, + expectRowVisible, + expectVisible, + test, +} from '@oxide/test' import { stopInstance } from './util' diff --git a/app/pages/__tests__/org-access.e2e.ts b/app/pages/__tests__/org-access.e2e.ts index c92cb87d05..67d0493f8d 100644 --- a/app/pages/__tests__/org-access.e2e.ts +++ b/app/pages/__tests__/org-access.e2e.ts @@ -1,6 +1,4 @@ -import { test } from '@playwright/test' - -import { expectNotVisible, expectRowVisible, expectVisible } from 'app/util/e2e' +import { expectNotVisible, expectRowVisible, expectVisible, test } from '@oxide/test' test('Click through org access page', async ({ page }) => { await page.goto('/orgs/maze-war') diff --git a/app/pages/__tests__/orgs.e2e.ts b/app/pages/__tests__/orgs.e2e.ts index 7623c12515..2a1ac6b14c 100644 --- a/app/pages/__tests__/orgs.e2e.ts +++ b/app/pages/__tests__/orgs.e2e.ts @@ -1,7 +1,4 @@ -import { test } from '@playwright/test' - -import { createOrg, genName } from 'app/test/helpers' -import { expectVisible } from 'app/util/e2e' +import { expectVisible, test } from '@oxide/test' test('Root to orgs redirect', async ({ page }) => { await page.goto('/') @@ -9,7 +6,7 @@ test('Root to orgs redirect', async ({ page }) => { await expectVisible(page, ['role=heading[name="Organizations"]']) }) -test('Create org and navigate to project', async ({ page }) => { +test('Create org and navigate to project', async ({ page, createOrg }) => { await page.goto('/orgs') await expectVisible(page, ['role=heading[name="Organizations"]']) @@ -23,18 +20,15 @@ test('Create org and navigate to project', async ({ page }) => { ]) await page.goBack() - const orgName = genName('org-create-test') - const removeOrg = await createOrg(page, { - name: orgName, + const org = await createOrg({ + name: 'org-create-test', description: 'used to test org creation', }) // org page (redirects to /org/org-name/projects) - await page.click(`role=link[name="${orgName}"]`) + await page.click(`role=link[name="${org.name}"]`) await expectVisible(page, [ 'role=heading[name="Projects"]', 'role=heading[name="No projects"]', ]) - - await removeOrg() }) diff --git a/app/pages/__tests__/project-access.e2e.ts b/app/pages/__tests__/project-access.e2e.ts index 626700a899..b37e255b79 100644 --- a/app/pages/__tests__/project-access.e2e.ts +++ b/app/pages/__tests__/project-access.e2e.ts @@ -1,6 +1,4 @@ -import { test } from '@playwright/test' - -import { expectNotVisible, expectRowVisible, expectVisible } from 'app/util/e2e' +import { expectNotVisible, expectRowVisible, expectVisible, test } from '@oxide/test' test('Click through project access page', async ({ page }) => { await page.goto('/orgs/maze-war/projects/mock-project') diff --git a/app/pages/__tests__/project-create.e2e.tsx b/app/pages/__tests__/project-create.e2e.tsx index 9013e03a67..ead75df539 100644 --- a/app/pages/__tests__/project-create.e2e.tsx +++ b/app/pages/__tests__/project-create.e2e.tsx @@ -1,6 +1,4 @@ -import { expect, test } from '@playwright/test' - -import { expectVisible } from 'app/util/e2e' +import { expect, expectVisible, test } from '@oxide/test' test.describe('Project create', () => { test.beforeEach(async ({ page }) => { diff --git a/app/pages/__tests__/project-selector.e2e.ts b/app/pages/__tests__/project-selector.e2e.ts index 8e2b759490..fa183fb6cd 100644 --- a/app/pages/__tests__/project-selector.e2e.ts +++ b/app/pages/__tests__/project-selector.e2e.ts @@ -1,6 +1,4 @@ -import { expect, test } from '@playwright/test' - -import { expectVisible } from 'app/util/e2e' +import { expect, expectVisible, test } from '@oxide/test' test('Project selector', async ({ page }) => { // create a second project diff --git a/app/pages/__tests__/row-select.e2e.ts b/app/pages/__tests__/row-select.e2e.ts index c5deac6631..c0c92c9984 100644 --- a/app/pages/__tests__/row-select.e2e.ts +++ b/app/pages/__tests__/row-select.e2e.ts @@ -1,6 +1,4 @@ -import { expect, test } from '@playwright/test' - -import { forEach } from 'app/util/e2e' +import { expect, forEach, test } from '@oxide/test' // This could easily be done as a testing-lib test but I want it in a real // table. The .is-selected asserts are slightly brittle (and contrary to our diff --git a/app/pages/__tests__/ssh-keys.e2e.ts b/app/pages/__tests__/ssh-keys.e2e.ts index ebccd913c7..7d96dfcdfd 100644 --- a/app/pages/__tests__/ssh-keys.e2e.ts +++ b/app/pages/__tests__/ssh-keys.e2e.ts @@ -1,6 +1,4 @@ -import { test } from '@playwright/test' - -import { expectNotVisible, expectRowVisible, expectVisible } from 'app/util/e2e' +import { expectNotVisible, expectRowVisible, expectVisible, test } from '@oxide/test' test('SSH keys', async ({ page }) => { await page.goto('/settings/ssh-keys') diff --git a/app/test/helpers.ts b/app/test/helpers.ts deleted file mode 100644 index 443460fe07..0000000000 --- a/app/test/helpers.ts +++ /dev/null @@ -1,147 +0,0 @@ -import type { Page, Response } from '@playwright/test' - -import type { - OrganizationCreate, - OrganizationDeleteParams, - ProjectCreate, - ProjectCreateParams, - ProjectDeleteParams, -} from '@oxide/api' - -import { expectNotVisible } from 'app/util/e2e' - -type Ok = [T, null] -type Err = [null, E] -type Result = Ok | Err -type ReturnFn = () => Promise - -// imitate Rust's Result constructors -const Ok = (o: T): Ok => [o, null] -const Err = (err: E): Err => [null, err] - -interface ReqError { - code: number - msg: string -} - -// TODO: This is duplicated from `@oxide/api` and can't be pulled in due to -// inlined tests using `import.meta` being imported and causing a syntax error -// given that playwright converts all the tests to commonjs. -export const genName = (...parts: [string, ...string[]]) => { - const numParts = parts.length - const partLength = Math.floor(63 / numParts) - Math.ceil(6 / numParts) - 1 - return ( - parts - .map((part) => part.substring(0, partLength)) - .join('-') - // generate random hex string of 6 characters - .concat(`-${Math.random().toString(16).substring(2, 8)}`) - ) -} - -const goto = async (page: Page, url: string): Promise> => { - const currentUrl = page.url() - const response = await page.goto(url) - if (!response) throw new Error(`No response recieved for request to ${url}`) - const status = response.status() - if (status > 399) { - return Err({ - msg: `Loading ${url} failed with a status code of ${status}`, - code: status, - }) - } - return Ok(() => page.goto(currentUrl)) -} - -export async function checkIfExists(page: Page, path: string) { - const res = await page.request.get(path) - const status = res.status() - if (status >= 500) { - throw new Error(`${path} failed to load with a ${status} response code`) - } - return status >= 200 && status < 300 -} - -// --- Organizations --------- - -/** - * Creates an Organization and returns to the page it was called from. - */ -export async function createOrg(page: Page, body: OrganizationCreate) { - const [back, err] = await goto(page, '/orgs/new') - if (err) { - throw new Error(err.msg) - } - await page.fill('role=textbox[name="Name"]', body.name) - await page.fill('role=textbox[name="Description"]', body.description) - await page.click('role=button[name="Create organization"]') - await page.waitForNavigation() - await back() - return () => { - return deleteOrg(page, { orgName: body.name }) - } -} - -export async function deleteOrg(page: Page, params: OrganizationDeleteParams) { - const [back, err] = await goto(page, '/orgs') - if (err) { - throw new Error(err.msg) - } - - await page - .locator('role=row', { hasText: params.orgName }) - .locator('role=button[name="Row actions"]') - .click() - await page.click('role=menuitem[name="Delete"]') - await expectNotVisible(page, [`role=cell[name="${params.orgName}"]`]) - - await back() -} - -// --- Projects -------------- - -/* - * Creates a project (and its parent org if it doesn't already exist) then returns - * to the page it was called from. - */ -export async function createProject( - page: Page, - params: ProjectCreateParams, - body: ProjectCreate -) { - let orgCreated = false - if (!checkIfExists(page, `/orgs/${params.orgName}`)) { - await createOrg(page, { name: params.orgName, description: '' }) - orgCreated = true - } - const [back, err] = await goto(page, `/orgs/${params.orgName}/projects/new`) - if (err) { - throw new Error(err.msg) - } - await page.fill('role=textbox[name="Name"]', body.name) - await page.fill('role=textbox[name="Description"]', body.description) - await back!() - - return async () => { - await deleteProject(page, { ...params, projectName: body.name }) - if (orgCreated) { - await deleteOrg(page, params) - } - } -} - -export async function deleteProject(page: Page, params: ProjectDeleteParams) { - const [back, err] = await goto(page, `/orgs/${params.orgName}/projects`) - if (err) { - throw new Error(err.msg) - } - - await page - .locator('role=row', { hasText: params.projectName }) - .locator('role=button[name="Row actions"]') - .click() - await page.click('role=menuitem[name="Delete"]') - await expectNotVisible(page, [`role=cell[name="${params.projectName}"]`]) - - await back() -} diff --git a/libs/test/fixtures.ts b/libs/test/fixtures.ts new file mode 100644 index 0000000000..35689c0f43 --- /dev/null +++ b/libs/test/fixtures.ts @@ -0,0 +1,93 @@ +import type { Page } from '@playwright/test' + +import type { + OrganizationCreate, + OrganizationDeleteParams, + ProjectCreate, + ProjectCreateParams, + ProjectDeleteParams, +} from '@oxide/api' + +import { expectNotVisible } from './utils' + +function interceptArgs, B extends unknown>( + f: (...args: A) => B, + callback: (...args: A) => A +): (...args: A) => B { + return new Proxy(f, { + apply(target, context, args) { + return target.apply(context, callback(...(args as A))) + }, + }) +} + +const goto = async (page: Page, url: string) => { + const currentUrl = page.url() + const response = await page.goto(url) + if (!response) throw new Error(`No response recieved for request to ${url}`) + const status = response.status() + if (status > 399) { + throw new Error(`Loading ${url} failed with a status code of ${status}`) + } + return () => page.goto(currentUrl) +} + +/** + * Creates an Organization and returns to the page it was called from. + */ +export const createOrg = ( + page: Page, + callback: (body: OrganizationCreate) => [body: OrganizationCreate] +) => + interceptArgs(async (body: OrganizationCreate) => { + const back = await goto(page, '/orgs/new') + + await page.fill('role=textbox[name="Name"]', body.name) + await page.fill('role=textbox[name="Description"]', body.description) + await page.click('role=button[name="Create organization"]') + await page.waitForNavigation() + await back() + + return body + }, callback) + +export const deleteOrg = (page: Page) => async (params: OrganizationDeleteParams) => { + const back = await goto(page, '/orgs') + + await page + .locator('role=row', { hasText: params.orgName }) + .locator('role=button[name="Row actions"]') + .click() + await page.click('role=menuitem[name="Delete"]') + await expectNotVisible(page, [`role=cell[name="${params.orgName}"]`]) + + await back() +} + +export const createProject = ( + page: Page, + callback: ( + params: ProjectCreateParams, + body: ProjectCreate + ) => [ProjectCreateParams, ProjectCreate] +) => + interceptArgs(async (params: ProjectCreateParams, body: ProjectCreate) => { + const back = await goto(page, `/orgs/${params.orgName}/projects/new`) + await page.fill('role=textbox[name="Name"]', body.name) + await page.fill('role=textbox[name="Description"]', body.description) + await page.click('role=button[name="Create project"]') + await back() + }, callback) + +export const deleteProject = (page: Page) => async (params: ProjectDeleteParams) => { + const back = await goto(page, `/orgs/${params.orgName}/projects`) + + await page + .locator('role=row', { hasText: params.projectName }) + .locator('role=button[name="Row actions"]') + .click() + await page.click('role=menuitem[name="Delete"]') + await expectNotVisible(page, [`role=cell[name="${params.projectName}"]`]) + + await back() +} diff --git a/libs/test/index.ts b/libs/test/index.ts new file mode 100644 index 0000000000..88c2f10a45 --- /dev/null +++ b/libs/test/index.ts @@ -0,0 +1,55 @@ +import { test as base } from '@playwright/test' + +import type { ProjectDeleteParams } from '@oxide/api' + +import { createProject, deleteProject } from './fixtures' +import { createOrg, deleteOrg } from './fixtures' +import { genName } from './utils' + +interface Fixtures { + createOrg: ReturnType + deleteOrg: ReturnType + createProject: ReturnType + deleteProject: ReturnType +} + +export * from '@playwright/test' +export * from './utils' + +export const test = base.extend({ + async createOrg({ page, deleteOrg }, use) { + const orgsToRemove: string[] = [] + await use( + createOrg(page, ({ name, ...others }) => { + const newName = genName(name) + orgsToRemove.push(newName) + return [{ name: newName, ...others }] + }) + ) + for (const org of orgsToRemove) { + await deleteOrg({ orgName: org }) + } + }, + + async deleteOrg({ page }, use) { + await use(deleteOrg(page)) + }, + + async createProject({ page, deleteProject }, use) { + const projectsToRemove: ProjectDeleteParams[] = [] + await use( + createProject(page, (params, { name, ...others }) => { + const newName = genName(name) + projectsToRemove.push({ ...params, projectName: newName }) + return [params, { name: newName, ...others }] + }) + ) + for (const params of projectsToRemove) { + await deleteProject(params) + } + }, + + async deleteProject({ page }, use) { + await use(deleteProject(page)) + }, +}) diff --git a/app/util/e2e.ts b/libs/test/utils.ts similarity index 79% rename from app/util/e2e.ts rename to libs/test/utils.ts index a530605516..fc2103148f 100644 --- a/app/util/e2e.ts +++ b/libs/test/utils.ts @@ -70,3 +70,18 @@ export async function expectRowVisible( .poll(getRows) .toEqual(expect.arrayContaining([expect.objectContaining(expectedRow)])) } + +// TODO: This is duplicated from `@oxide/api` and can't be pulled in due to +// inlined tests using `import.meta` being imported and causing a syntax error +// given that playwright converts all the tests to commonjs. +export const genName = (...parts: [string, ...string[]]) => { + const numParts = parts.length + const partLength = Math.floor(63 / numParts) - Math.ceil(6 / numParts) - 1 + return ( + parts + .map((part) => part.substring(0, partLength)) + .join('-') + // generate random hex string of 6 characters + .concat(`-${Math.random().toString(16).substring(2, 8)}`) + ) +} diff --git a/tsconfig.json b/tsconfig.json index f35dca197f..6deb6a9ec8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,6 +22,7 @@ "@oxide/ui": ["libs/ui/index.ts"], "@oxide/util": ["libs/util/index.ts"], "@oxide/table": ["libs/table/index.ts"], + "@oxide/test": ["libs/test/index.ts"], "@oxide/pagination": ["libs/pagination/index.ts"] }, "resolveJsonModule": true, From 897262b923091a5464f212c6a4ef582951ad482f Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 18 Aug 2022 13:34:20 -0400 Subject: [PATCH 13/69] Kill @oxide/test --- app/forms/__tests__/instance-create.e2e.ts | 2 +- app/pages/__tests__/NotFound.e2e.ts | 2 +- app/pages/__tests__/click-everything.e2e.ts | 2 +- .../__tests__/instance/attach-disk.e2e.ts | 2 +- .../__tests__/instance/networking.e2e.ts | 2 +- app/pages/__tests__/org-access.e2e.ts | 2 +- app/pages/__tests__/orgs.e2e.ts | 2 +- app/pages/__tests__/project-access.e2e.ts | 2 +- app/pages/__tests__/project-create.e2e.tsx | 2 +- app/pages/__tests__/project-selector.e2e.ts | 2 +- app/pages/__tests__/row-select.e2e.ts | 2 +- app/pages/__tests__/ssh-keys.e2e.ts | 2 +- {libs/test => app/test/e2e}/fixtures.ts | 0 app/test/e2e/index.ts | 0 {libs/test => app/test/e2e}/utils.ts | 0 app/test/{ => unit}/server.ts | 0 app/test/{ => unit}/setup.ts | 0 app/test/{ => unit}/utils.tsx | 0 libs/test/index.ts | 55 ------------------- tsconfig.json | 1 - 20 files changed, 12 insertions(+), 68 deletions(-) rename {libs/test => app/test/e2e}/fixtures.ts (100%) create mode 100644 app/test/e2e/index.ts rename {libs/test => app/test/e2e}/utils.ts (100%) rename app/test/{ => unit}/server.ts (100%) rename app/test/{ => unit}/setup.ts (100%) rename app/test/{ => unit}/utils.tsx (100%) delete mode 100644 libs/test/index.ts diff --git a/app/forms/__tests__/instance-create.e2e.ts b/app/forms/__tests__/instance-create.e2e.ts index 2c52e5f7a2..d733fda32f 100644 --- a/app/forms/__tests__/instance-create.e2e.ts +++ b/app/forms/__tests__/instance-create.e2e.ts @@ -1,4 +1,4 @@ -import { expect, expectVisible, test } from '@oxide/test' +import { expect, expectVisible, test } from 'app/test/e2e' test.describe('Instance Create Form', () => { test('can invoke instance create form from instances page', async ({ page }) => { diff --git a/app/pages/__tests__/NotFound.e2e.ts b/app/pages/__tests__/NotFound.e2e.ts index f3d247654c..669996e8da 100644 --- a/app/pages/__tests__/NotFound.e2e.ts +++ b/app/pages/__tests__/NotFound.e2e.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@oxide/test' +import { expect, test } from 'app/test/e2e' test('Shows 404 page when a resource is not found', async ({ page }) => { await page.goto('/orgs/nonexistent') diff --git a/app/pages/__tests__/click-everything.e2e.ts b/app/pages/__tests__/click-everything.e2e.ts index 043f85ad98..db6b86b61b 100644 --- a/app/pages/__tests__/click-everything.e2e.ts +++ b/app/pages/__tests__/click-everything.e2e.ts @@ -1,4 +1,4 @@ -import { expectNotVisible, expectVisible, test } from '@oxide/test' +import { expectNotVisible, expectVisible, test } from 'app/test/e2e' test("Click through everything and make it's all there", async ({ page }) => { await page.goto('/orgs/maze-war/projects') diff --git a/app/pages/__tests__/instance/attach-disk.e2e.ts b/app/pages/__tests__/instance/attach-disk.e2e.ts index 6fd57e318d..2741340fe4 100644 --- a/app/pages/__tests__/instance/attach-disk.e2e.ts +++ b/app/pages/__tests__/instance/attach-disk.e2e.ts @@ -1,4 +1,4 @@ -import { expectVisible, test } from '@oxide/test' +import { expectVisible, test } from 'app/test/e2e' import { stopInstance } from './util' diff --git a/app/pages/__tests__/instance/networking.e2e.ts b/app/pages/__tests__/instance/networking.e2e.ts index 39d6703544..b32e3bb87b 100644 --- a/app/pages/__tests__/instance/networking.e2e.ts +++ b/app/pages/__tests__/instance/networking.e2e.ts @@ -4,7 +4,7 @@ import { expectRowVisible, expectVisible, test, -} from '@oxide/test' +} from 'app/test/e2e' import { stopInstance } from './util' diff --git a/app/pages/__tests__/org-access.e2e.ts b/app/pages/__tests__/org-access.e2e.ts index 67d0493f8d..0dc7dd8d2d 100644 --- a/app/pages/__tests__/org-access.e2e.ts +++ b/app/pages/__tests__/org-access.e2e.ts @@ -1,4 +1,4 @@ -import { expectNotVisible, expectRowVisible, expectVisible, test } from '@oxide/test' +import { expectNotVisible, expectRowVisible, expectVisible, test } from 'app/test/e2e' test('Click through org access page', async ({ page }) => { await page.goto('/orgs/maze-war') diff --git a/app/pages/__tests__/orgs.e2e.ts b/app/pages/__tests__/orgs.e2e.ts index 2a1ac6b14c..0a78f6356f 100644 --- a/app/pages/__tests__/orgs.e2e.ts +++ b/app/pages/__tests__/orgs.e2e.ts @@ -1,4 +1,4 @@ -import { expectVisible, test } from '@oxide/test' +import { expectVisible, test } from 'app/test/e2e' test('Root to orgs redirect', async ({ page }) => { await page.goto('/') diff --git a/app/pages/__tests__/project-access.e2e.ts b/app/pages/__tests__/project-access.e2e.ts index b37e255b79..f60cfcbe68 100644 --- a/app/pages/__tests__/project-access.e2e.ts +++ b/app/pages/__tests__/project-access.e2e.ts @@ -1,4 +1,4 @@ -import { expectNotVisible, expectRowVisible, expectVisible, test } from '@oxide/test' +import { expectNotVisible, expectRowVisible, expectVisible, test } from 'app/test/e2e' test('Click through project access page', async ({ page }) => { await page.goto('/orgs/maze-war/projects/mock-project') diff --git a/app/pages/__tests__/project-create.e2e.tsx b/app/pages/__tests__/project-create.e2e.tsx index ead75df539..cf6ae2556d 100644 --- a/app/pages/__tests__/project-create.e2e.tsx +++ b/app/pages/__tests__/project-create.e2e.tsx @@ -1,4 +1,4 @@ -import { expect, expectVisible, test } from '@oxide/test' +import { expect, expectVisible, test } from 'app/test/e2e' test.describe('Project create', () => { test.beforeEach(async ({ page }) => { diff --git a/app/pages/__tests__/project-selector.e2e.ts b/app/pages/__tests__/project-selector.e2e.ts index fa183fb6cd..cbeda84fa5 100644 --- a/app/pages/__tests__/project-selector.e2e.ts +++ b/app/pages/__tests__/project-selector.e2e.ts @@ -1,4 +1,4 @@ -import { expect, expectVisible, test } from '@oxide/test' +import { expect, expectVisible, test } from 'app/test/e2e' test('Project selector', async ({ page }) => { // create a second project diff --git a/app/pages/__tests__/row-select.e2e.ts b/app/pages/__tests__/row-select.e2e.ts index c0c92c9984..3af6832336 100644 --- a/app/pages/__tests__/row-select.e2e.ts +++ b/app/pages/__tests__/row-select.e2e.ts @@ -1,4 +1,4 @@ -import { expect, forEach, test } from '@oxide/test' +import { expect, forEach, test } from 'app/test/e2e' // This could easily be done as a testing-lib test but I want it in a real // table. The .is-selected asserts are slightly brittle (and contrary to our diff --git a/app/pages/__tests__/ssh-keys.e2e.ts b/app/pages/__tests__/ssh-keys.e2e.ts index 7d96dfcdfd..eb39fcbfdd 100644 --- a/app/pages/__tests__/ssh-keys.e2e.ts +++ b/app/pages/__tests__/ssh-keys.e2e.ts @@ -1,4 +1,4 @@ -import { expectNotVisible, expectRowVisible, expectVisible, test } from '@oxide/test' +import { expectNotVisible, expectRowVisible, expectVisible, test } from 'app/test/e2e' test('SSH keys', async ({ page }) => { await page.goto('/settings/ssh-keys') diff --git a/libs/test/fixtures.ts b/app/test/e2e/fixtures.ts similarity index 100% rename from libs/test/fixtures.ts rename to app/test/e2e/fixtures.ts diff --git a/app/test/e2e/index.ts b/app/test/e2e/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/test/utils.ts b/app/test/e2e/utils.ts similarity index 100% rename from libs/test/utils.ts rename to app/test/e2e/utils.ts diff --git a/app/test/server.ts b/app/test/unit/server.ts similarity index 100% rename from app/test/server.ts rename to app/test/unit/server.ts diff --git a/app/test/setup.ts b/app/test/unit/setup.ts similarity index 100% rename from app/test/setup.ts rename to app/test/unit/setup.ts diff --git a/app/test/utils.tsx b/app/test/unit/utils.tsx similarity index 100% rename from app/test/utils.tsx rename to app/test/unit/utils.tsx diff --git a/libs/test/index.ts b/libs/test/index.ts deleted file mode 100644 index 88c2f10a45..0000000000 --- a/libs/test/index.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { test as base } from '@playwright/test' - -import type { ProjectDeleteParams } from '@oxide/api' - -import { createProject, deleteProject } from './fixtures' -import { createOrg, deleteOrg } from './fixtures' -import { genName } from './utils' - -interface Fixtures { - createOrg: ReturnType - deleteOrg: ReturnType - createProject: ReturnType - deleteProject: ReturnType -} - -export * from '@playwright/test' -export * from './utils' - -export const test = base.extend({ - async createOrg({ page, deleteOrg }, use) { - const orgsToRemove: string[] = [] - await use( - createOrg(page, ({ name, ...others }) => { - const newName = genName(name) - orgsToRemove.push(newName) - return [{ name: newName, ...others }] - }) - ) - for (const org of orgsToRemove) { - await deleteOrg({ orgName: org }) - } - }, - - async deleteOrg({ page }, use) { - await use(deleteOrg(page)) - }, - - async createProject({ page, deleteProject }, use) { - const projectsToRemove: ProjectDeleteParams[] = [] - await use( - createProject(page, (params, { name, ...others }) => { - const newName = genName(name) - projectsToRemove.push({ ...params, projectName: newName }) - return [params, { name: newName, ...others }] - }) - ) - for (const params of projectsToRemove) { - await deleteProject(params) - } - }, - - async deleteProject({ page }, use) { - await use(deleteProject(page)) - }, -}) diff --git a/tsconfig.json b/tsconfig.json index 6deb6a9ec8..f35dca197f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,7 +22,6 @@ "@oxide/ui": ["libs/ui/index.ts"], "@oxide/util": ["libs/util/index.ts"], "@oxide/table": ["libs/table/index.ts"], - "@oxide/test": ["libs/test/index.ts"], "@oxide/pagination": ["libs/pagination/index.ts"] }, "resolveJsonModule": true, From ac4d580d54fe2b14b16ae15be94308d0624b019f Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 18 Aug 2022 13:45:26 -0400 Subject: [PATCH 14/69] Naming cleanup --- app/test/e2e/index.ts | 55 ++++++++++++++++++++++++++ app/test/unit/{utils.tsx => index.tsx} | 0 libs/api/__tests__/hooks.spec.tsx | 2 +- 3 files changed, 56 insertions(+), 1 deletion(-) rename app/test/unit/{utils.tsx => index.tsx} (100%) diff --git a/app/test/e2e/index.ts b/app/test/e2e/index.ts index e69de29bb2..88c2f10a45 100644 --- a/app/test/e2e/index.ts +++ b/app/test/e2e/index.ts @@ -0,0 +1,55 @@ +import { test as base } from '@playwright/test' + +import type { ProjectDeleteParams } from '@oxide/api' + +import { createProject, deleteProject } from './fixtures' +import { createOrg, deleteOrg } from './fixtures' +import { genName } from './utils' + +interface Fixtures { + createOrg: ReturnType + deleteOrg: ReturnType + createProject: ReturnType + deleteProject: ReturnType +} + +export * from '@playwright/test' +export * from './utils' + +export const test = base.extend({ + async createOrg({ page, deleteOrg }, use) { + const orgsToRemove: string[] = [] + await use( + createOrg(page, ({ name, ...others }) => { + const newName = genName(name) + orgsToRemove.push(newName) + return [{ name: newName, ...others }] + }) + ) + for (const org of orgsToRemove) { + await deleteOrg({ orgName: org }) + } + }, + + async deleteOrg({ page }, use) { + await use(deleteOrg(page)) + }, + + async createProject({ page, deleteProject }, use) { + const projectsToRemove: ProjectDeleteParams[] = [] + await use( + createProject(page, (params, { name, ...others }) => { + const newName = genName(name) + projectsToRemove.push({ ...params, projectName: newName }) + return [params, { name: newName, ...others }] + }) + ) + for (const params of projectsToRemove) { + await deleteProject(params) + } + }, + + async deleteProject({ page }, use) { + await use(deleteProject(page)) + }, +}) diff --git a/app/test/unit/utils.tsx b/app/test/unit/index.tsx similarity index 100% rename from app/test/unit/utils.tsx rename to app/test/unit/index.tsx diff --git a/libs/api/__tests__/hooks.spec.tsx b/libs/api/__tests__/hooks.spec.tsx index 871729e907..54f066ec9b 100644 --- a/libs/api/__tests__/hooks.spec.tsx +++ b/libs/api/__tests__/hooks.spec.tsx @@ -4,7 +4,7 @@ import { act, renderHook } from '@testing-library/react-hooks' import { org } from '@oxide/api-mocks' -import { overrideOnce } from 'app/test/utils' +import { overrideOnce } from 'app/test/unit' import type { ApiError } from '..' import { useApiMutation, useApiQuery } from '..' From 82ef223595c660dfec23169e535b80b2c50c04dd Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 18 Aug 2022 14:23:38 -0400 Subject: [PATCH 15/69] Cleanup fixtures --- app/pages/__tests__/orgs.e2e.ts | 9 ++- app/test/e2e/fixtures.ts | 139 ++++++++++++++++---------------- app/test/e2e/index.ts | 53 +----------- 3 files changed, 77 insertions(+), 124 deletions(-) diff --git a/app/pages/__tests__/orgs.e2e.ts b/app/pages/__tests__/orgs.e2e.ts index 0a78f6356f..e870f06bae 100644 --- a/app/pages/__tests__/orgs.e2e.ts +++ b/app/pages/__tests__/orgs.e2e.ts @@ -1,4 +1,4 @@ -import { expectVisible, test } from 'app/test/e2e' +import { expectVisible, genName, test } from 'app/test/e2e' test('Root to orgs redirect', async ({ page }) => { await page.goto('/') @@ -20,13 +20,14 @@ test('Create org and navigate to project', async ({ page, createOrg }) => { ]) await page.goBack() - const org = await createOrg({ - name: 'org-create-test', + const name = genName('org-create-test') + await createOrg({ + name, description: 'used to test org creation', }) // org page (redirects to /org/org-name/projects) - await page.click(`role=link[name="${org.name}"]`) + await page.click(`role=link[name="${name}"]`) await expectVisible(page, [ 'role=heading[name="Projects"]', 'role=heading[name="No projects"]', diff --git a/app/test/e2e/fixtures.ts b/app/test/e2e/fixtures.ts index 35689c0f43..6957312906 100644 --- a/app/test/e2e/fixtures.ts +++ b/app/test/e2e/fixtures.ts @@ -1,4 +1,5 @@ import type { Page } from '@playwright/test' +import { test as base } from '@playwright/test' import type { OrganizationCreate, @@ -10,17 +11,6 @@ import type { import { expectNotVisible } from './utils' -function interceptArgs, B extends unknown>( - f: (...args: A) => B, - callback: (...args: A) => A -): (...args: A) => B { - return new Proxy(f, { - apply(target, context, args) { - return target.apply(context, callback(...(args as A))) - }, - }) -} - const goto = async (page: Page, url: string) => { const currentUrl = page.url() const response = await page.goto(url) @@ -32,62 +22,75 @@ const goto = async (page: Page, url: string) => { return () => page.goto(currentUrl) } -/** - * Creates an Organization and returns to the page it was called from. - */ -export const createOrg = ( - page: Page, - callback: (body: OrganizationCreate) => [body: OrganizationCreate] -) => - interceptArgs(async (body: OrganizationCreate) => { - const back = await goto(page, '/orgs/new') - - await page.fill('role=textbox[name="Name"]', body.name) - await page.fill('role=textbox[name="Description"]', body.description) - await page.click('role=button[name="Create organization"]') - await page.waitForNavigation() - await back() - - return body - }, callback) - -export const deleteOrg = (page: Page) => async (params: OrganizationDeleteParams) => { - const back = await goto(page, '/orgs') - - await page - .locator('role=row', { hasText: params.orgName }) - .locator('role=button[name="Row actions"]') - .click() - await page.click('role=menuitem[name="Delete"]') - await expectNotVisible(page, [`role=cell[name="${params.orgName}"]`]) - - await back() +interface Fixtures { + createOrg: (body: OrganizationCreate) => Promise + deleteOrg: (params: OrganizationDeleteParams) => Promise + createProject: (params: ProjectCreateParams, body: ProjectCreate) => Promise + deleteProject: (params: ProjectDeleteParams) => Promise } -export const createProject = ( - page: Page, - callback: ( - params: ProjectCreateParams, - body: ProjectCreate - ) => [ProjectCreateParams, ProjectCreate] -) => - interceptArgs(async (params: ProjectCreateParams, body: ProjectCreate) => { - const back = await goto(page, `/orgs/${params.orgName}/projects/new`) - await page.fill('role=textbox[name="Name"]', body.name) - await page.fill('role=textbox[name="Description"]', body.description) - await page.click('role=button[name="Create project"]') - await back() - }, callback) - -export const deleteProject = (page: Page) => async (params: ProjectDeleteParams) => { - const back = await goto(page, `/orgs/${params.orgName}/projects`) - - await page - .locator('role=row', { hasText: params.projectName }) - .locator('role=button[name="Row actions"]') - .click() - await page.click('role=menuitem[name="Delete"]') - await expectNotVisible(page, [`role=cell[name="${params.projectName}"]`]) - - await back() -} +export const test = base.extend({ + async createOrg({ page, deleteOrg }, use) { + const orgsToRemove: string[] = [] + + await use(async (body) => { + const back = await goto(page, '/orgs/new') + + await page.fill('role=textbox[name="Name"]', body.name) + await page.fill('role=textbox[name="Description"]', body.description) + await page.click('role=button[name="Create organization"]') + await page.waitForNavigation() + await back() + }) + + for (const org of orgsToRemove) { + await deleteOrg({ orgName: org }) + } + }, + + async deleteOrg({ page }, use) { + await use(async (params: OrganizationDeleteParams) => { + const back = await goto(page, '/orgs') + + await page + .locator('role=row', { hasText: params.orgName }) + .locator('role=button[name="Row actions"]') + .click() + await page.click('role=menuitem[name="Delete"]') + await expectNotVisible(page, [`role=cell[name="${params.orgName}"]`]) + + await back() + }) + }, + + async createProject({ page, deleteProject }, use) { + const projectsToRemove: ProjectDeleteParams[] = [] + + await use(async (params, body) => { + const back = await goto(page, `/orgs/${params.orgName}/projects/new`) + await page.fill('role=textbox[name="Name"]', body.name) + await page.fill('role=textbox[name="Description"]', body.description) + await page.click('role=button[name="Create project"]') + await back() + }) + + for (const params of projectsToRemove) { + await deleteProject(params) + } + }, + + async deleteProject({ page }, use) { + await use(async (params: ProjectDeleteParams) => { + const back = await goto(page, `/orgs/${params.orgName}/projects`) + + await page + .locator('role=row', { hasText: params.projectName }) + .locator('role=button[name="Row actions"]') + .click() + await page.click('role=menuitem[name="Delete"]') + await expectNotVisible(page, [`role=cell[name="${params.projectName}"]`]) + + await back() + }) + }, +}) diff --git a/app/test/e2e/index.ts b/app/test/e2e/index.ts index 88c2f10a45..d4e7a4d086 100644 --- a/app/test/e2e/index.ts +++ b/app/test/e2e/index.ts @@ -1,55 +1,4 @@ -import { test as base } from '@playwright/test' - -import type { ProjectDeleteParams } from '@oxide/api' - -import { createProject, deleteProject } from './fixtures' -import { createOrg, deleteOrg } from './fixtures' -import { genName } from './utils' - -interface Fixtures { - createOrg: ReturnType - deleteOrg: ReturnType - createProject: ReturnType - deleteProject: ReturnType -} - export * from '@playwright/test' export * from './utils' -export const test = base.extend({ - async createOrg({ page, deleteOrg }, use) { - const orgsToRemove: string[] = [] - await use( - createOrg(page, ({ name, ...others }) => { - const newName = genName(name) - orgsToRemove.push(newName) - return [{ name: newName, ...others }] - }) - ) - for (const org of orgsToRemove) { - await deleteOrg({ orgName: org }) - } - }, - - async deleteOrg({ page }, use) { - await use(deleteOrg(page)) - }, - - async createProject({ page, deleteProject }, use) { - const projectsToRemove: ProjectDeleteParams[] = [] - await use( - createProject(page, (params, { name, ...others }) => { - const newName = genName(name) - projectsToRemove.push({ ...params, projectName: newName }) - return [params, { name: newName, ...others }] - }) - ) - for (const params of projectsToRemove) { - await deleteProject(params) - } - }, - - async deleteProject({ page }, use) { - await use(deleteProject(page)) - }, -}) +export { test } from './fixtures' From 1f42a30a0a994ce7da49d28d3eb2046230617c8f Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 18 Aug 2022 14:39:04 -0400 Subject: [PATCH 16/69] Fix vite config --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index 69f43efa61..1a3501eee2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -70,7 +70,7 @@ export default defineConfig(({ mode }) => ({ test: { globals: true, environment: 'jsdom', - setupFiles: ['app/test/setup.ts'], + setupFiles: ['app/test/unit/setup.ts'], includeSource: ['libs/util/*.ts'], }, })) From 4a61dd04de0db215480d687c563841e22d6abc21 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 18 Aug 2022 14:52:31 -0400 Subject: [PATCH 17/69] Create org for project if it doesn't exist --- app/test/e2e/fixtures.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/app/test/e2e/fixtures.ts b/app/test/e2e/fixtures.ts index 6957312906..22d84c09ab 100644 --- a/app/test/e2e/fixtures.ts +++ b/app/test/e2e/fixtures.ts @@ -23,6 +23,7 @@ const goto = async (page: Page, url: string) => { } interface Fixtures { + resourceExists: (url: string) => Promise createOrg: (body: OrganizationCreate) => Promise deleteOrg: (params: OrganizationDeleteParams) => Promise createProject: (params: ProjectCreateParams, body: ProjectCreate) => Promise @@ -30,6 +31,16 @@ interface Fixtures { } export const test = base.extend({ + async resourceExists({ page }, use) { + await use(async (url) => { + const res = await page.request.get(url) + const status = res.status() + if (status >= 500) { + throw new Error(`${url} failed to load with a ${status} response code`) + } + return status >= 200 && status < 400 + }) + }, async createOrg({ page, deleteOrg }, use) { const orgsToRemove: string[] = [] @@ -63,10 +74,13 @@ export const test = base.extend({ }) }, - async createProject({ page, deleteProject }, use) { + async createProject({ page, deleteProject, resourceExists, createOrg }, use) { const projectsToRemove: ProjectDeleteParams[] = [] await use(async (params, body) => { + if (!(await resourceExists(`/orgs/${params.orgName}`))) { + await createOrg({ name: params.orgName, description: '' }) + } const back = await goto(page, `/orgs/${params.orgName}/projects/new`) await page.fill('role=textbox[name="Name"]', body.name) await page.fill('role=textbox[name="Description"]', body.description) From 807579edac33d9fe862671c8d81b7c8c84027ac6 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 18 Aug 2022 16:31:15 -0400 Subject: [PATCH 18/69] Convert project switcher test --- app/pages/__tests__/project-selector.e2e.ts | 62 +++++++++++++-------- app/test/e2e/fixtures.ts | 16 +----- 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/app/pages/__tests__/project-selector.e2e.ts b/app/pages/__tests__/project-selector.e2e.ts index cbeda84fa5..cb7dc4e9fe 100644 --- a/app/pages/__tests__/project-selector.e2e.ts +++ b/app/pages/__tests__/project-selector.e2e.ts @@ -1,41 +1,57 @@ -import { expect, expectVisible, test } from 'app/test/e2e' +import { expect, expectVisible, genName, test } from 'app/test/e2e' -test('Project selector', async ({ page }) => { - // create a second project - await page.goto('/orgs/maze-war/projects/new') - await page.fill('role=textbox[name="Name"]', 'other-project') - await page.click('role=button[name="Create project"]') +test('Project selector', async ({ page, createOrg, createProject }) => { + const orgName = genName('project-selector-org') + const p1Name = genName('project-a') + const p2Name = genName('project-b') - // go to projects page and make sure they're both there - await page.click('role=link[name="Projects"]') - await expect(page).toHaveURL('/orgs/maze-war/projects') - await expectVisible(page, [ - 'role=cell[name="mock-project"]', - 'role=cell[name="other-project"]', - ]) + await createOrg({ name: orgName, description: 'project selector test' }) + + // Create 1st project + await createProject( + { orgName }, + { + name: p1Name, + description: 'First project', + } + ) + + // Create 2nd project + await createProject( + { orgName }, + { + name: p2Name, + description: 'Second project', + } + ) + + // Go to the projects page + await page.goto(`/orgs/${orgName}/projects`) + + await expectVisible(page, [`role=cell[name="${p1Name}"]`, `role=cell[name="${p2Name}"]`]) // switcher button is present, has text indicating no project selected await expect(page.locator('role=button[name="Switch project"]')).toHaveText( - 'maze-warselect a project' + `${orgName}select a project` ) await page.click('role=button[name="Switch project"]') await expectVisible(page, [ - 'role=menuitem[name="mock-project"]', - 'role=menuitem[name="other-project"]', + `role=menuitem[name="${p1Name}"]`, + `role=menuitem[name="${p2Name}"]`, ]) - // picking mock-project in the menu takes you there - await page.click('role=menuitem[name="mock-project"]') - await expect(page).toHaveURL('/orgs/maze-war/projects/mock-project/instances') + // picking p1 in the menu takes you there + await page.click(`role=menuitem[name="${p1Name}"]`) + await expect(page).toHaveURL(`/orgs/${orgName}/projects/${p1Name}/instances`) await expect(page.locator('role=button[name="Switch project"]')).toHaveText( - 'maze-warmock-project' + `${orgName}${p1Name}` ) // picking other-project in the menu takes you there await page.click('role=button[name="Switch project"]') - await page.click('role=menuitem[name="other-project"]') - await expect(page).toHaveURL('/orgs/maze-war/projects/other-project/instances') + await page.click(`role=menuitem[name="${p2Name}"]`) + await expect(page).toHaveURL(`/orgs/${orgName}/projects/${p2Name}/instances`) await expect(page.locator('role=button[name="Switch project"]')).toHaveText( - 'maze-warother-project' + `${orgName}${p2Name}` ) }) diff --git a/app/test/e2e/fixtures.ts b/app/test/e2e/fixtures.ts index 22d84c09ab..6957312906 100644 --- a/app/test/e2e/fixtures.ts +++ b/app/test/e2e/fixtures.ts @@ -23,7 +23,6 @@ const goto = async (page: Page, url: string) => { } interface Fixtures { - resourceExists: (url: string) => Promise createOrg: (body: OrganizationCreate) => Promise deleteOrg: (params: OrganizationDeleteParams) => Promise createProject: (params: ProjectCreateParams, body: ProjectCreate) => Promise @@ -31,16 +30,6 @@ interface Fixtures { } export const test = base.extend({ - async resourceExists({ page }, use) { - await use(async (url) => { - const res = await page.request.get(url) - const status = res.status() - if (status >= 500) { - throw new Error(`${url} failed to load with a ${status} response code`) - } - return status >= 200 && status < 400 - }) - }, async createOrg({ page, deleteOrg }, use) { const orgsToRemove: string[] = [] @@ -74,13 +63,10 @@ export const test = base.extend({ }) }, - async createProject({ page, deleteProject, resourceExists, createOrg }, use) { + async createProject({ page, deleteProject }, use) { const projectsToRemove: ProjectDeleteParams[] = [] await use(async (params, body) => { - if (!(await resourceExists(`/orgs/${params.orgName}`))) { - await createOrg({ name: params.orgName, description: '' }) - } const back = await goto(page, `/orgs/${params.orgName}/projects/new`) await page.fill('role=textbox[name="Name"]', body.name) await page.fill('role=textbox[name="Description"]', body.description) From d9cb025c32540960a7143857963b65fc7c6ef9fa Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 18 Aug 2022 16:56:10 -0400 Subject: [PATCH 19/69] Remove all debouncing from store --- libs/api-mocks/msw/store.ts | 30 ++---------------------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/libs/api-mocks/msw/store.ts b/libs/api-mocks/msw/store.ts index 5b9d9a9410..fd8c41bf61 100644 --- a/libs/api-mocks/msw/store.ts +++ b/libs/api-mocks/msw/store.ts @@ -8,7 +8,6 @@ import { clone } from './util' interface StoreOptions> { initialValues: T store?: Storage - debounceTime?: number } /** @@ -23,9 +22,7 @@ export function createStore>( key: string, options: StoreOptions ) { - const debounceTime = options.debounceTime ?? 100 let cache = {} as T - let timer: NodeJS.Timeout | null = null const store = options.store ?? window.localStorage // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -51,12 +48,12 @@ export function createStore>( }, set(target, name, value) { target[name] = proxify(value) - debounceWrite() + write() return true }, deleteProperty(target, name) { delete target[name] - debounceWrite() + write() return true }, }) @@ -65,24 +62,9 @@ export function createStore>( } const write = () => { - if (timer) { - clearTimeout(timer) - timer = null - } store[key] = JSON.stringify(cache) } - const debounceWrite = () => { - if (debounceTime === 0) { - write() - return - } - if (timer) { - clearTimeout(timer) - } - timer = setTimeout(write, debounceTime) - } - const clear = () => { cache = clone(options.initialValues) proxify(cache) @@ -90,10 +72,6 @@ export function createStore>( } Object.defineProperty(cache, 'clear', { value: clear }) - const unload = () => { - if (timer) write() - } - try { Object.assign(cache, JSON.parse(store[key])) } catch { @@ -102,9 +80,5 @@ export function createStore>( cache = proxify(cache) write() - if (debounceTime > 0) { - window.addEventListener('unload', unload) - } - return cache as T & { clear: () => void } } From 56b7668da5098e8516ced83b74b4d62a41fe8805 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 18 Aug 2022 21:48:09 -0400 Subject: [PATCH 20/69] Use sessionStorage instead --- libs/api-mocks/msw/store.ts | 2 +- package.json | 4 +- yarn.lock | 127 ++++++++++++++++++++++++++++++++---- 3 files changed, 117 insertions(+), 16 deletions(-) diff --git a/libs/api-mocks/msw/store.ts b/libs/api-mocks/msw/store.ts index fd8c41bf61..645ef915fc 100644 --- a/libs/api-mocks/msw/store.ts +++ b/libs/api-mocks/msw/store.ts @@ -23,7 +23,7 @@ export function createStore>( options: StoreOptions ) { let cache = {} as T - const store = options.store ?? window.localStorage + const store = options.store ?? window.sessionStorage // eslint-disable-next-line @typescript-eslint/no-explicit-any const proxify = (obj: any) => { diff --git a/package.json b/package.json index df280e5dfd..0d97564493 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "@types/uuid": "^8.3.0", "@typescript-eslint/eslint-plugin": "^5.30.0", "@typescript-eslint/parser": "^5.30.0", - "@vitejs/plugin-react": "^2.0.0", + "@vitejs/plugin-react": "^2.0.1", "autoprefixer": "^10.2.5", "babel-loader": "^8.2.2", "chromatic": "^6.4.2", @@ -133,7 +133,7 @@ "type-fest": "^2.17.0", "typescript": "4.7.4", "url-loader": "^4.1.1", - "vite": "^3.0.4", + "vite": "^3.0.8", "vitest": "^0.22.1", "webpack": "^5.73.0", "whatwg-fetch": "^3.6.2" diff --git a/yarn.lock b/yarn.lock index 360086f46a..3214841a18 100644 --- a/yarn.lock +++ b/yarn.lock @@ -131,7 +131,28 @@ json5 "^2.2.1" semver "^6.3.0" -"@babel/core@^7.18.6", "@babel/core@^7.18.9": +"@babel/core@^7.18.10": + version "7.18.10" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.10.tgz#39ad504991d77f1f3da91be0b8b949a5bc466fb8" + integrity sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw== + dependencies: + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.18.10" + "@babel/helper-compilation-targets" "^7.18.9" + "@babel/helper-module-transforms" "^7.18.9" + "@babel/helpers" "^7.18.9" + "@babel/parser" "^7.18.10" + "@babel/template" "^7.18.10" + "@babel/traverse" "^7.18.10" + "@babel/types" "^7.18.10" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.1" + semver "^6.3.0" + +"@babel/core@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.9.tgz#805461f967c77ff46c74ca0460ccf4fe933ddd59" integrity sha512-1LIb1eL8APMy91/IMW+31ckrfBM4yCoLaVzoDhZUKSM4cu1L1nIidyxkCgzPAgrC5WEz36IPEr/eSeSF9pIn+g== @@ -188,6 +209,15 @@ "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" +"@babel/generator@^7.18.10": + version "7.18.12" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.12.tgz#fa58daa303757bd6f5e4bbca91b342040463d9f4" + integrity sha512-dfQ8ebCN98SvyL7IxNMCUtZQSq5R7kxgN+r8qYTGDmmSion1hX2C0zq2yo1bsCDhXixokv1SAWTZUMYbO/V5zg== + dependencies: + "@babel/types" "^7.18.10" + "@jridgewell/gen-mapping" "^0.3.2" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@^7.10.4", "@babel/helper-annotate-as-pure@^7.12.13": version "7.12.13" resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.13.tgz" @@ -612,6 +642,11 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-string-parser@^7.18.10": + version "7.18.10" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz#181f22d28ebe1b3857fa575f5c290b1aaf659b56" + integrity sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw== + "@babel/helper-validator-identifier@^7.12.11", "@babel/helper-validator-identifier@^7.14.5", "@babel/helper-validator-identifier@^7.14.9", "@babel/helper-validator-identifier@^7.15.7": version "7.15.7" resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz" @@ -741,6 +776,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.9.tgz#f2dde0c682ccc264a9a8595efd030a5cc8fd2539" integrity sha512-9uJveS9eY9DJ0t64YbIBZICtJy8a5QrDEVdiLCG97fVLpDTpGX7t8mMSb6OWw6Lrnjqj4O8zwjELX3dhoMgiBg== +"@babel/parser@^7.18.10", "@babel/parser@^7.18.11": + version "7.18.11" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.11.tgz#68bb07ab3d380affa9a3f96728df07969645d2d9" + integrity sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2" @@ -1661,6 +1701,17 @@ "@babel/plugin-syntax-jsx" "^7.12.13" "@babel/types" "^7.14.2" +"@babel/plugin-transform-react-jsx@^7.18.10": + version "7.18.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.18.10.tgz#ea47b2c4197102c196cbd10db9b3bb20daa820f1" + integrity sha512-gCy7Iikrpu3IZjYZolFE4M1Sm+nrh1/6za2Ewj77Z+XirT4TsbJcvOFOyF+fRPwU6AKKK136CZxx6L8AbSFG6A== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/plugin-syntax-jsx" "^7.18.6" + "@babel/types" "^7.18.10" + "@babel/plugin-transform-react-jsx@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.18.6.tgz#2721e96d31df96e3b7ad48ff446995d26bc028ff" @@ -2169,6 +2220,15 @@ "@babel/parser" "^7.16.7" "@babel/types" "^7.16.7" +"@babel/template@^7.18.10": + version "7.18.10" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71" + integrity sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/parser" "^7.18.10" + "@babel/types" "^7.18.10" + "@babel/template@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.6.tgz#1283f4993e00b929d6e2d3c72fdc9168a2977a31" @@ -2241,6 +2301,22 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.18.10": + version "7.18.11" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.11.tgz#3d51f2afbd83ecf9912bcbb5c4d94e3d2ddaa16f" + integrity sha512-TG9PiM2R/cWCAy6BPJKeHzNbu4lPzOSZpeMfeNErskGpTJx6trEvFaVCbDvpcxwy49BKWmEPwiW8mrysNiDvIQ== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.18.10" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.18.9" + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/parser" "^7.18.11" + "@babel/types" "^7.18.10" + debug "^4.1.0" + globals "^11.1.0" + "@babel/types@7.17.0", "@babel/types@^7.0.0", "@babel/types@^7.16.7", "@babel/types@^7.17.0", "@babel/types@^7.3.0": version "7.17.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.0.tgz#a826e368bccb6b3d84acd76acad5c0d87342390b" @@ -2265,6 +2341,15 @@ "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" +"@babel/types@^7.18.10": + version "7.18.10" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.10.tgz#4908e81b6b339ca7c6b7a555a5fc29446f26dde6" + integrity sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ== + dependencies: + "@babel/helper-string-parser" "^7.18.10" + "@babel/helper-validator-identifier" "^7.18.6" + to-fast-properties "^2.0.0" + "@babel/types@^7.18.6", "@babel/types@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.9.tgz#7148d64ba133d8d73a41b3172ac4b83a1452205f" @@ -4566,13 +4651,13 @@ "@typescript-eslint/types" "5.9.1" eslint-visitor-keys "^3.0.0" -"@vitejs/plugin-react@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-2.0.0.tgz#12decd097773a00620e44b780b1d2c00df101449" - integrity sha512-zHkRR+X4zqEPNBbKV2FvWSxK7Q6crjMBVIAYroSU8Nbb4M3E5x4qOiLoqJBHtXgr27kfednXjkwr3lr8jS6Wrw== +"@vitejs/plugin-react@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-2.0.1.tgz#3197c01d8e4a4eb9fed829c7888c467a43aadd4e" + integrity sha512-uINzNHmjrbunlFtyVkST6lY1ewSfz/XwLufG0PIqvLGnpk2nOIOa/1CACTDNcKi1/RwaCzJLmsXwm1NsUVV/NA== dependencies: - "@babel/core" "^7.18.6" - "@babel/plugin-transform-react-jsx" "^7.18.6" + "@babel/core" "^7.18.10" + "@babel/plugin-transform-react-jsx" "^7.18.10" "@babel/plugin-transform-react-jsx-development" "^7.18.6" "@babel/plugin-transform-react-jsx-self" "^7.18.6" "@babel/plugin-transform-react-jsx-source" "^7.18.6" @@ -12493,6 +12578,15 @@ postcss@^8.4.14: picocolors "^1.0.0" source-map-js "^1.0.2" +postcss@^8.4.16: + version "8.4.16" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.16.tgz#33a1d675fac39941f5f445db0de4db2b6e01d43c" + integrity sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ== + dependencies: + nanoid "^3.3.4" + picocolors "^1.0.0" + source-map-js "^1.0.2" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" @@ -13518,6 +13612,13 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" +"rollup@>=2.75.6 <2.77.0 || ~2.77.0": + version "2.77.3" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.77.3.tgz#8f00418d3a2740036e15deb653bed1a90ee0cc12" + integrity sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g== + optionalDependencies: + fsevents "~2.3.2" + rollup@^2.75.6: version "2.77.0" resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.77.0.tgz#749eaa5ac09b6baa52acc076bc46613eddfd53f4" @@ -15397,15 +15498,15 @@ vfile@^4.0.0: optionalDependencies: fsevents "~2.3.2" -vite@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/vite/-/vite-3.0.4.tgz#c61688d6b97573e96cf5ac25f2d68597b5ce68e8" - integrity sha512-NU304nqnBeOx2MkQnskBQxVsa0pRAH5FphokTGmyy8M3oxbvw7qAXts2GORxs+h/2vKsD+osMhZ7An6yK6F1dA== +vite@^3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/vite/-/vite-3.0.8.tgz#aa095ad8e3e5da46d9ec7e878f262678965d6531" + integrity sha512-AOZ4eN7mrkJiOLuw8IA7piS4IdOQyQCA81GxGsAQvAZzMRi9ZwGB3TOaYsj4uLAWK46T5L4AfQ6InNGlxX30IQ== dependencies: esbuild "^0.14.47" - postcss "^8.4.14" + postcss "^8.4.16" resolve "^1.22.1" - rollup "^2.75.6" + rollup ">=2.75.6 <2.77.0 || ~2.77.0" optionalDependencies: fsevents "~2.3.2" From 26f04e658b96aac65033b111eeefe0c699ec5e79 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Sat, 20 Aug 2022 02:43:25 -0400 Subject: [PATCH 21/69] Use dedicated mock server for e2e tests --- .../project/networking/VpcPage/VpcPage.e2e.ts | 2 +- app/test/e2e/global-setup.ts | 39 +++++++++ app/test/e2e/global-teardown.ts | 8 ++ libs/api-mocks/msw/db.ts | 11 +-- libs/api-mocks/msw/store.ts | 84 ------------------- playwright.config.ts | 4 +- 6 files changed, 57 insertions(+), 91 deletions(-) create mode 100644 app/test/e2e/global-setup.ts create mode 100644 app/test/e2e/global-teardown.ts delete mode 100644 libs/api-mocks/msw/store.ts diff --git a/app/pages/project/networking/VpcPage/VpcPage.e2e.ts b/app/pages/project/networking/VpcPage/VpcPage.e2e.ts index 267e3bd984..be4e2f6f15 100644 --- a/app/pages/project/networking/VpcPage/VpcPage.e2e.ts +++ b/app/pages/project/networking/VpcPage/VpcPage.e2e.ts @@ -10,7 +10,7 @@ test.describe('VpcPage', () => { await expect(page.locator('text=mock-subnet')).toBeVisible() }) - test('can create subnet', async ({ page }) => { + test.only('can create subnet', async ({ page }) => { await page.goto('/orgs/maze-war/projects/mock-project/vpcs/mock-vpc') // only one row in table, the default mock-subnet const rows = await page.locator('tbody >> tr') diff --git a/app/test/e2e/global-setup.ts b/app/test/e2e/global-setup.ts new file mode 100644 index 0000000000..268c6bc252 --- /dev/null +++ b/app/test/e2e/global-setup.ts @@ -0,0 +1,39 @@ +/** + * https://playwright.dev/docs/test-advanced#global-setup-and-teardown + */ +import http from 'http' +import { setupServer } from 'msw/node' + +import { handlers } from '@oxide/api-mocks' + +export const server = setupServer( + // Serverside handlers _must_ have a host + ...handlers.map((h) => { + h.info.path = 'http://localhost' + h.info.path + return h + }) +) + +export default async function globalSetup() { + server.listen() + // Creates a passthrough http server at 12220 (Nexus' normal port). Requests + // from the console will come here and the on request handler will forward the + // request to localhost... but that forwarded request will be intercepted by MSW + http + .createServer() + .listen(12220) + .on('request', (req, res) => { + const connector = http.request( + { + host: 'localhost', + path: 'api' + req.url, + method: req.method, + headers: req.headers, + }, + (resp) => { + resp.pipe(res) + } + ) + req.pipe(connector) + }) +} diff --git a/app/test/e2e/global-teardown.ts b/app/test/e2e/global-teardown.ts new file mode 100644 index 0000000000..3e7a0b593a --- /dev/null +++ b/app/test/e2e/global-teardown.ts @@ -0,0 +1,8 @@ +/** + * https://playwright.dev/docs/test-advanced#global-setup-and-teardown + */ +import { server } from './global-setup' + +export default async function globalTeardown() { + server.close() +} diff --git a/libs/api-mocks/msw/db.ts b/libs/api-mocks/msw/db.ts index 2b4e5cdbc7..e515f98944 100644 --- a/libs/api-mocks/msw/db.ts +++ b/libs/api-mocks/msw/db.ts @@ -6,7 +6,6 @@ import type { ApiTypes as Api } from '@oxide/api' import { sessionMe } from '@oxide/api-mocks' import type { Json } from '../json-type' -import { createStore } from './store' import { clone, json } from './util' const notFoundBody = { error_code: 'ObjectNotFound' } as const @@ -167,10 +166,12 @@ const initDb = { vpcSubnets: [mock.vpcSubnet], } -export const db = createStore('msw-db', { - initialValues: clone(initDb), -}) +// export const db = createStore('msw-db', { +// initialValues: clone(initDb), +// }) + +export let db = clone(initDb) export function resetDb() { - db.clear() + db = clone(initDb) } diff --git a/libs/api-mocks/msw/store.ts b/libs/api-mocks/msw/store.ts deleted file mode 100644 index 645ef915fc..0000000000 --- a/libs/api-mocks/msw/store.ts +++ /dev/null @@ -1,84 +0,0 @@ -/** - * This is a fork of https://github.com/ropg/storage-as-an-object - * storage-as-an-object is MIT license which can be found here: - * https://github.com/ropg/storage-as-an-object/blob/7a12a1dd3bcc1b87a77e8202fae98ae25323b968/LICENSE - */ -import { clone } from './util' - -interface StoreOptions> { - initialValues: T - store?: Storage -} - -/** - * createStore Creates an object that's persisted to a storage target like - * localStorage or sessionStorage - * - * @param {string} key - The name of the string in the store - * @param {StoreOptions} [options] - Optional configuration - * @returns {Object} - The object that your code interfaces with - */ -export function createStore>( - key: string, - options: StoreOptions -) { - let cache = {} as T - const store = options.store ?? window.sessionStorage - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const proxify = (obj: any) => { - const validTypes = ['string', 'number', 'object', 'undefined', 'boolean'] - if (!validTypes.includes(typeof obj)) { - throw new Error('StorageObject does not store this variable type') - } - if (typeof obj === 'object' && obj !== null) { - if (obj instanceof Date) { - return { __DATE: obj.toJSON() } - } - for (const key in obj) { - obj[key] = proxify(obj[key]) - } - return new Proxy(obj, { - get(target, name) { - const val = target[name] - if (val && typeof val === 'object' && val.__DATE) { - return new Date(val.__DATE) - } - return val - }, - set(target, name, value) { - target[name] = proxify(value) - write() - return true - }, - deleteProperty(target, name) { - delete target[name] - write() - return true - }, - }) - } - return obj - } - - const write = () => { - store[key] = JSON.stringify(cache) - } - - const clear = () => { - cache = clone(options.initialValues) - proxify(cache) - write() - } - Object.defineProperty(cache, 'clear', { value: clear }) - - try { - Object.assign(cache, JSON.parse(store[key])) - } catch { - Object.assign(cache, clone(options.initialValues)) - } - cache = proxify(cache) - write() - - return cache as T & { clear: () => void } -} diff --git a/playwright.config.ts b/playwright.config.ts index 6e55bf9644..9fe75723a0 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -13,6 +13,8 @@ const config: PlaywrightTestConfig = { retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, + globalSetup: 'app/test/e2e/global-setup.ts', + globalTeardown: 'app/test/e2e/global-teardown.ts', use: { /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ baseURL: 'http://localhost:4009', @@ -36,7 +38,7 @@ const config: PlaywrightTestConfig = { // use different port so it doesn't conflict with local dev server webServer: { - command: 'yarn start:msw --port 4009', + command: 'yarn start --port 4009', port: 4009, }, } From 99637980f909fee5f588c9241b015f0ab5d894bd Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Sat, 20 Aug 2022 02:45:49 -0400 Subject: [PATCH 22/69] Revert some unneeded changes --- app/msw-mock-api.ts | 9 --------- libs/api/__tests__/safety.spec.ts | 2 +- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/app/msw-mock-api.ts b/app/msw-mock-api.ts index 140c3e86fd..1d23f93cee 100644 --- a/app/msw-mock-api.ts +++ b/app/msw-mock-api.ts @@ -1,5 +1,3 @@ -import { resetDb } from '@oxide/api-mocks' - function getChaos() { const chaos = parseFloat(process.env.CHAOS || '') return Number.isNaN(chaos) ? null : chaos @@ -38,13 +36,6 @@ export async function startMockAPI() { const { handlers } = await import('@oxide/api-mocks') const { setupWorker, rest, compose } = await import('msw') - const params = window.location.search.slice(1).split('&') - if (params.includes('clear')) { - console.log('resetting db...') - resetDb() - window.location.search = params.filter((p) => p !== 'clear').join('&') - } - // defined in here because it depends on the dynamic import const chaosInterceptor = rest.all('/api/*', (_req, res, ctx) => { if (shouldFail(chaos)) { diff --git a/libs/api/__tests__/safety.spec.ts b/libs/api/__tests__/safety.spec.ts index ee68190e97..303ad9e2af 100644 --- a/libs/api/__tests__/safety.spec.ts +++ b/libs/api/__tests__/safety.spec.ts @@ -27,7 +27,7 @@ const grepFiles = (s: string) => it('@oxide/api-mocks is only referenced in test files', () => { const files = grepFiles("from '@oxide/api-mocks'") for (const file of files) { - expect(file).toMatch(/__tests__\/|app\/test\/|\.spec\.|tsconfig|api-mocks|msw-mock-api/) + expect(file).toMatch(/__tests__\/|app\/test\/|\.spec\.|tsconfig|api-mocks/) } }) From 1bfdc8d1edeaec4707f7c977ad51660aabd3dc4e Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Sat, 20 Aug 2022 02:47:36 -0400 Subject: [PATCH 23/69] Revert another unneeded change --- app/test/unit/setup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/test/unit/setup.ts b/app/test/unit/setup.ts index 1b0c2030fa..aee836f791 100644 --- a/app/test/unit/setup.ts +++ b/app/test/unit/setup.ts @@ -12,7 +12,7 @@ import { resetDb } from '@oxide/api-mocks' import { server } from './server' beforeAll(() => server.listen()) -afterEach(async () => { +afterEach(() => { resetDb() server.resetHandlers() }) From 876a9d255a3a31d80593a95063a1eb3746aca9a0 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Sat, 20 Aug 2022 03:11:28 -0400 Subject: [PATCH 24/69] Remove stray only --- app/pages/project/networking/VpcPage/VpcPage.e2e.ts | 2 +- app/test/e2e/fixtures.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/pages/project/networking/VpcPage/VpcPage.e2e.ts b/app/pages/project/networking/VpcPage/VpcPage.e2e.ts index be4e2f6f15..267e3bd984 100644 --- a/app/pages/project/networking/VpcPage/VpcPage.e2e.ts +++ b/app/pages/project/networking/VpcPage/VpcPage.e2e.ts @@ -10,7 +10,7 @@ test.describe('VpcPage', () => { await expect(page.locator('text=mock-subnet')).toBeVisible() }) - test.only('can create subnet', async ({ page }) => { + test('can create subnet', async ({ page }) => { await page.goto('/orgs/maze-war/projects/mock-project/vpcs/mock-vpc') // only one row in table, the default mock-subnet const rows = await page.locator('tbody >> tr') diff --git a/app/test/e2e/fixtures.ts b/app/test/e2e/fixtures.ts index 6957312906..c4e4a31574 100644 --- a/app/test/e2e/fixtures.ts +++ b/app/test/e2e/fixtures.ts @@ -39,7 +39,6 @@ export const test = base.extend({ await page.fill('role=textbox[name="Name"]', body.name) await page.fill('role=textbox[name="Description"]', body.description) await page.click('role=button[name="Create organization"]') - await page.waitForNavigation() await back() }) From 438103938b10f43f9a88e615d89dce4cef098ba8 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Sat, 20 Aug 2022 15:43:59 -0400 Subject: [PATCH 25/69] Improve fixture api, start on vpcs tests --- app/pages/__tests__/orgs.e2e.ts | 3 +- app/pages/__tests__/project-selector.e2e.ts | 18 +--- .../project/networking/VpcPage/VpcPage.e2e.ts | 21 +++-- app/test/e2e/fixtures.ts | 87 ++++++++++++++----- 4 files changed, 83 insertions(+), 46 deletions(-) diff --git a/app/pages/__tests__/orgs.e2e.ts b/app/pages/__tests__/orgs.e2e.ts index e870f06bae..b73f356b09 100644 --- a/app/pages/__tests__/orgs.e2e.ts +++ b/app/pages/__tests__/orgs.e2e.ts @@ -21,8 +21,7 @@ test('Create org and navigate to project', async ({ page, createOrg }) => { await page.goBack() const name = genName('org-create-test') - await createOrg({ - name, + await createOrg(name, { description: 'used to test org creation', }) diff --git a/app/pages/__tests__/project-selector.e2e.ts b/app/pages/__tests__/project-selector.e2e.ts index cb7dc4e9fe..dece529b0a 100644 --- a/app/pages/__tests__/project-selector.e2e.ts +++ b/app/pages/__tests__/project-selector.e2e.ts @@ -5,25 +5,13 @@ test('Project selector', async ({ page, createOrg, createProject }) => { const p1Name = genName('project-a') const p2Name = genName('project-b') - await createOrg({ name: orgName, description: 'project selector test' }) + await createOrg(orgName) // Create 1st project - await createProject( - { orgName }, - { - name: p1Name, - description: 'First project', - } - ) + await createProject(orgName, p1Name) // Create 2nd project - await createProject( - { orgName }, - { - name: p2Name, - description: 'Second project', - } - ) + await createProject(orgName, p2Name) // Go to the projects page await page.goto(`/orgs/${orgName}/projects`) diff --git a/app/pages/project/networking/VpcPage/VpcPage.e2e.ts b/app/pages/project/networking/VpcPage/VpcPage.e2e.ts index 267e3bd984..ace800cfa5 100644 --- a/app/pages/project/networking/VpcPage/VpcPage.e2e.ts +++ b/app/pages/project/networking/VpcPage/VpcPage.e2e.ts @@ -1,13 +1,24 @@ -import { expect, test } from '@playwright/test' +import { expect, genName, test } from 'app/test/e2e' test.describe('VpcPage', () => { + const orgName = genName('vpc-page-org') + const projectName = genName('vpc-page-project') + const vpcName = genName('mock-vpc') + + test.beforeEach(async ({ createOrg, createProject, createVpc }) => { + await createOrg(orgName) + await createProject(orgName, projectName) + await createVpc(orgName, projectName, vpcName) + }) + test('can nav to VpcPage from /', async ({ page }) => { await page.goto('/') - await page.click('table :text("maze-war")') - await page.click('table :text("mock-project")') + await page.pause() + await page.click(`table :text("${orgName}")`) + await page.click(`table :text("${projectName}")`) await page.click('a:has-text("Networking")') - await page.click('a:has-text("mock-vpc")') - await expect(page.locator('text=mock-subnet')).toBeVisible() + await page.click(`a:has-text("${vpcName}")`) + await expect(page).toHaveURL(`/orgs/${orgName}/projects/${projectName}/vpcs/${vpcName}`) }) test('can create subnet', async ({ page }) => { diff --git a/app/test/e2e/fixtures.ts b/app/test/e2e/fixtures.ts index c4e4a31574..d46ad6ce29 100644 --- a/app/test/e2e/fixtures.ts +++ b/app/test/e2e/fixtures.ts @@ -5,8 +5,9 @@ import type { OrganizationCreate, OrganizationDeleteParams, ProjectCreate, - ProjectCreateParams, ProjectDeleteParams, + VpcCreate, + VpcDeleteParams, } from '@oxide/api' import { expectNotVisible } from './utils' @@ -22,22 +23,36 @@ const goto = async (page: Page, url: string) => { return () => page.goto(currentUrl) } +type Body = Partial> + interface Fixtures { - createOrg: (body: OrganizationCreate) => Promise + createOrg: (orgName: string, body?: Body) => Promise deleteOrg: (params: OrganizationDeleteParams) => Promise - createProject: (params: ProjectCreateParams, body: ProjectCreate) => Promise + createProject: ( + orgName: string, + projectName: string, + body?: Body + ) => Promise deleteProject: (params: ProjectDeleteParams) => Promise + createVpc: ( + orgName: string, + projectName: string, + vpcName: string, + body?: Body + ) => Promise + deleteVpc: (params: VpcDeleteParams) => Promise + deleteTableRow: (rowText: string) => Promise } export const test = base.extend({ async createOrg({ page, deleteOrg }, use) { const orgsToRemove: string[] = [] - await use(async (body) => { + await use(async (orgName, body = {}) => { const back = await goto(page, '/orgs/new') - await page.fill('role=textbox[name="Name"]', body.name) - await page.fill('role=textbox[name="Description"]', body.description) + await page.fill('role=textbox[name="Name"]', orgName) + await page.fill('role=textbox[name="Description"]', body.description || '') await page.click('role=button[name="Create organization"]') await back() }) @@ -47,17 +62,10 @@ export const test = base.extend({ } }, - async deleteOrg({ page }, use) { + async deleteOrg({ page, deleteTableRow }, use) { await use(async (params: OrganizationDeleteParams) => { const back = await goto(page, '/orgs') - - await page - .locator('role=row', { hasText: params.orgName }) - .locator('role=button[name="Row actions"]') - .click() - await page.click('role=menuitem[name="Delete"]') - await expectNotVisible(page, [`role=cell[name="${params.orgName}"]`]) - + await deleteTableRow(params.orgName) await back() }) }, @@ -65,10 +73,10 @@ export const test = base.extend({ async createProject({ page, deleteProject }, use) { const projectsToRemove: ProjectDeleteParams[] = [] - await use(async (params, body) => { - const back = await goto(page, `/orgs/${params.orgName}/projects/new`) - await page.fill('role=textbox[name="Name"]', body.name) - await page.fill('role=textbox[name="Description"]', body.description) + await use(async (orgName, projectName, body = {}) => { + const back = await goto(page, `/orgs/${orgName}/projects/new`) + await page.fill('role=textbox[name="Name"]', projectName) + await page.fill('role=textbox[name="Description"]', body.description || '') await page.click('role=button[name="Create project"]') await back() }) @@ -78,18 +86,49 @@ export const test = base.extend({ } }, - async deleteProject({ page }, use) { + async deleteProject({ page, deleteTableRow }, use) { await use(async (params: ProjectDeleteParams) => { const back = await goto(page, `/orgs/${params.orgName}/projects`) + await deleteTableRow(params.projectName) + await back() + }) + }, + + async createVpc({ page, deleteVpc }, use) { + const vpcsToRemove: VpcDeleteParams[] = [] + await use(async (orgName, projectName, vpcName, body = {}) => { + const back = await goto(page, `/orgs/${orgName}/projects/${projectName}/vpcs/new`) + await page.fill('role=textbox[name="Name"]', vpcName) + await page.fill('role=textbox[name="Description"]', body.description || '') + await page.click('role=button[name="Create VPC"]') + await back() + }) + + for (const params of vpcsToRemove) { + await deleteVpc(params) + } + }, + + async deleteVpc({ page, deleteTableRow }, use) { + await use(async (params: VpcDeleteParams) => { + const back = await goto( + page, + `/orgs/${params.orgName}/projects/${params.projectName}/vpcs` + ) + await deleteTableRow(params.vpcName) + await back() + }) + }, + + async deleteTableRow({ page }, use) { + await use(async (rowText: string) => { await page - .locator('role=row', { hasText: params.projectName }) + .locator('role=row', { hasText: rowText }) .locator('role=button[name="Row actions"]') .click() await page.click('role=menuitem[name="Delete"]') - await expectNotVisible(page, [`role=cell[name="${params.projectName}"]`]) - - await back() + await expectNotVisible(page, [`role=cell[name="${rowText}"]`]) }) }, }) From 87fffa0088047cb92fe8a0cd9632422c774798b2 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 22 Aug 2022 17:16:04 -0400 Subject: [PATCH 26/69] Update handlers to match api --- .../project/networking/VpcPage/VpcPage.e2e.ts | 2 +- app/test/e2e/global-setup.ts | 40 +---- app/test/e2e/global-teardown.ts | 8 - libs/api-mocks/msw/handlers.ts | 166 +++++++++--------- package.json | 1 + playwright.config.ts | 1 - yarn.lock | 7 + 7 files changed, 103 insertions(+), 122 deletions(-) delete mode 100644 app/test/e2e/global-teardown.ts diff --git a/app/pages/project/networking/VpcPage/VpcPage.e2e.ts b/app/pages/project/networking/VpcPage/VpcPage.e2e.ts index ace800cfa5..9d3234db63 100644 --- a/app/pages/project/networking/VpcPage/VpcPage.e2e.ts +++ b/app/pages/project/networking/VpcPage/VpcPage.e2e.ts @@ -22,7 +22,7 @@ test.describe('VpcPage', () => { }) test('can create subnet', async ({ page }) => { - await page.goto('/orgs/maze-war/projects/mock-project/vpcs/mock-vpc') + await page.goto(`/orgs/${orgName}/projects/${projectName}/vpcs/${vpcName}`) // only one row in table, the default mock-subnet const rows = await page.locator('tbody >> tr') await expect(rows).toHaveCount(1) diff --git a/app/test/e2e/global-setup.ts b/app/test/e2e/global-setup.ts index 268c6bc252..1fcb407cc0 100644 --- a/app/test/e2e/global-setup.ts +++ b/app/test/e2e/global-setup.ts @@ -1,39 +1,17 @@ /** * https://playwright.dev/docs/test-advanced#global-setup-and-teardown */ -import http from 'http' -import { setupServer } from 'msw/node' +import { createServer } from '@mswjs/http-middleware' +import { expect } from '@playwright/test' import { handlers } from '@oxide/api-mocks' -export const server = setupServer( - // Serverside handlers _must_ have a host - ...handlers.map((h) => { - h.info.path = 'http://localhost' + h.info.path - return h - }) -) - export default async function globalSetup() { - server.listen() - // Creates a passthrough http server at 12220 (Nexus' normal port). Requests - // from the console will come here and the on request handler will forward the - // request to localhost... but that forwarded request will be intercepted by MSW - http - .createServer() - .listen(12220) - .on('request', (req, res) => { - const connector = http.request( - { - host: 'localhost', - path: 'api' + req.url, - method: req.method, - headers: req.headers, - }, - (resp) => { - resp.pipe(res) - } - ) - req.pipe(connector) - }) + // e2e tests should only run with a standalone server meaning MSW should _not_ be set + expect(process.env.MSW).toBeFalsy() + + // If pointing to a real nexus API don't mock + if (process.env.API_URL) { + createServer(...handlers).listen(12220) + } } diff --git a/app/test/e2e/global-teardown.ts b/app/test/e2e/global-teardown.ts deleted file mode 100644 index 3e7a0b593a..0000000000 --- a/app/test/e2e/global-teardown.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * https://playwright.dev/docs/test-advanced#global-setup-and-teardown - */ -import { server } from './global-setup' - -export default async function globalTeardown() { - server.close() -} diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index 9d97ad7438..deed695aba 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -77,23 +77,21 @@ type GetErr = NotFound | Unavailable type PostErr = AlreadyExists | NotFound export const handlers = [ - rest.get('/api/session/me', (req, res) => res(json(sessionMe))), - - rest.get>( - '/api/session/me/sshkeys', - (req, res) => - res( - json( - paginated( - req.url.search, - db.sshKeys.filter((key) => key.silo_user_id === sessionMe.id) - ) + rest.get('/session/me', (req, res) => res(json(sessionMe))), + + rest.get>('/session/me/sshkeys', (req, res) => + res( + json( + paginated( + req.url.search, + db.sshKeys.filter((key) => key.silo_user_id === sessionMe.id) ) ) + ) ), rest.post, never, Json | PostErr>( - '/api/session/me/sshkeys', + '/session/me/sshkeys', (req, res) => { const alreadyExists = db.sshKeys.some( (key) => key.name === req.body.name && key.silo_user_id === sessionMe.id @@ -121,7 +119,7 @@ export const handlers = [ ), rest.delete( - '/api/session/me/sshkeys/:sshKeyName', + '/session/me/sshkeys/:sshKeyName', (req, res, ctx) => { const [sshKey, err] = lookupSshKey(req.params) if (err) return res(err) @@ -130,7 +128,7 @@ export const handlers = [ } ), - rest.get | GetErr>('/api/policy', (req, res) => { + rest.get | GetErr>('/policy', (req, res) => { // assume we're in the default silo const siloId = defaultSilo.id const role_assignments = db.roleAssignments @@ -140,13 +138,12 @@ export const handlers = [ return res(json({ role_assignments })) }), - rest.get>( - '/api/organizations', - (req, res) => res(json(paginated(req.url.search, db.orgs))) + rest.get>('/organizations', (req, res) => + res(json(paginated(req.url.search, db.orgs))) ), rest.post, never, Json | PostErr>( - '/api/organizations', + '/organizations', (req, res) => { const alreadyExists = db.orgs.some((o) => o.name === req.body.name) if (alreadyExists) return res(alreadyExistsErr) @@ -166,7 +163,7 @@ export const handlers = [ ), rest.get | GetErr>( - '/api/organizations/:orgName', + '/organizations/:orgName', (req, res) => { if (req.params.orgName === '503') { return res(unavailableErr) @@ -180,7 +177,7 @@ export const handlers = [ ), rest.put, OrgParams, Json | PostErr>( - '/api/organizations/:orgName', + '/organizations/:orgName', (req, res) => { const [org, err] = lookupOrg(req.params) if (err) return res(err) @@ -196,7 +193,7 @@ export const handlers = [ ), rest.get | GetErr>( - '/api/organizations/:orgName/policy', + '/organizations/:orgName/policy', (req, res) => { const [org, err] = lookupOrg(req.params) if (err) return res(err) @@ -212,7 +209,7 @@ export const handlers = [ Json, ProjectParams, Json | PostErr - >('/api/organizations/:orgName/policy', (req, res) => { + >('/organizations/:orgName/policy', (req, res) => { const [org, err] = lookupOrg(req.params) if (err) return res(err) @@ -232,7 +229,7 @@ export const handlers = [ return res(json(req.body)) }), - rest.delete('/api/organizations/:orgName', (req, res, ctx) => { + rest.delete('/organizations/:orgName', (req, res, ctx) => { const [org, err] = lookupOrg(req.params) if (err) return res(err) db.orgs = db.orgs.filter((o) => o.id !== org.id) @@ -240,7 +237,7 @@ export const handlers = [ }), rest.get | GetErr>( - '/api/organizations/:orgName/projects', + '/organizations/:orgName/projects', (req, res) => { const [org, err] = lookupOrg(req.params) if (err) return res(err) @@ -251,7 +248,7 @@ export const handlers = [ ), rest.post, OrgParams, Json | PostErr>( - '/api/organizations/:orgName/projects', + '/organizations/:orgName/projects', (req, res) => { const [org, err] = lookupOrg(req.params) if (err) return res(err) @@ -278,7 +275,7 @@ export const handlers = [ ), rest.get | GetErr>( - '/api/organizations/:orgName/projects/:projectName', + '/organizations/:orgName/projects/:projectName', (req, res) => { const [project, err] = lookupProject(req.params) if (err) return res(err) @@ -287,7 +284,7 @@ export const handlers = [ ), rest.put, ProjectParams, Json | PostErr>( - '/api/organizations/:orgName/projects/:projectName', + '/organizations/:orgName/projects/:projectName', (req, res) => { const [project, err] = lookupProject(req.params) if (err) return res(err) @@ -303,7 +300,7 @@ export const handlers = [ ), rest.delete( - '/api/organizations/:orgName/projects/:projectName', + '/organizations/:orgName/projects/:projectName', (req, res, ctx) => { const [project, err] = lookupProject(req.params) if (err) return res(err) @@ -313,7 +310,7 @@ export const handlers = [ ), rest.get | GetErr>( - '/api/organizations/:orgName/projects/:projectName/policy', + '/organizations/:orgName/projects/:projectName/policy', (req, res) => { const [project, err] = lookupProject(req.params) if (err) return res(err) @@ -329,7 +326,7 @@ export const handlers = [ Json, ProjectParams, Json | PostErr - >('/api/organizations/:orgName/projects/:projectName/policy', (req, res) => { + >('/organizations/:orgName/projects/:projectName/policy', (req, res) => { const [project, err] = lookupProject(req.params) if (err) return res(err) @@ -350,7 +347,7 @@ export const handlers = [ }), rest.get | GetErr>( - '/api/organizations/:orgName/projects/:projectName/instances', + '/organizations/:orgName/projects/:projectName/instances', (req, res) => { const [project, err] = lookupProject(req.params) if (err) return res(err) @@ -360,7 +357,7 @@ export const handlers = [ ), rest.get | GetErr>( - '/api/organizations/:orgName/projects/:projectName/instances/:instanceName', + '/organizations/:orgName/projects/:projectName/instances/:instanceName', (req, res) => { const [instance, err] = lookupInstance(req.params) if (err) return res(err) @@ -369,7 +366,7 @@ export const handlers = [ ), rest.delete( - '/api/organizations/:orgName/projects/:projectName/instances/:instanceName', + '/organizations/:orgName/projects/:projectName/instances/:instanceName', (req, res, ctx) => { const [instance, err] = lookupInstance(req.params) if (err) return res(err) @@ -379,7 +376,7 @@ export const handlers = [ ), rest.post, ProjectParams, Json | PostErr>( - '/api/organizations/:orgName/projects/:projectName/instances', + '/organizations/:orgName/projects/:projectName/instances', (req, res) => { const [project, err] = lookupProject(req.params) if (err) return res(err) @@ -407,7 +404,7 @@ export const handlers = [ ), rest.post | PostErr>( - '/api/organizations/:orgName/projects/:projectName/instances/:instanceName/start', + '/organizations/:orgName/projects/:projectName/instances/:instanceName/start', (req, res) => { const [instance, err] = lookupInstance(req.params) if (err) return res(err) @@ -417,7 +414,7 @@ export const handlers = [ ), rest.post | PostErr>( - '/api/organizations/:orgName/projects/:projectName/instances/:instanceName/stop', + '/organizations/:orgName/projects/:projectName/instances/:instanceName/stop', (req, res) => { const [instance, err] = lookupInstance(req.params) if (err) return res(err) @@ -427,7 +424,7 @@ export const handlers = [ ), rest.get | GetErr>( - '/api/organizations/:orgName/projects/:projectName/instances/:instanceName/disks', + '/organizations/:orgName/projects/:projectName/instances/:instanceName/disks', (req, res) => { const [instance, err] = lookupInstance(req.params) if (err) return res(err) @@ -439,7 +436,7 @@ export const handlers = [ ), rest.post, InstanceParams, Json | PostErr>( - '/api/organizations/:orgName/projects/:projectName/instances/:instanceName/disks/attach', + '/organizations/:orgName/projects/:projectName/instances/:instanceName/disks/attach', (req, res) => { const [instance, instanceErr] = lookupInstance(req.params) if (instanceErr) return res(instanceErr) @@ -457,7 +454,7 @@ export const handlers = [ ), rest.post, InstanceParams, Json | PostErr>( - '/api/organizations/:orgName/projects/:projectName/instances/:instanceName/disks/detach', + '/organizations/:orgName/projects/:projectName/instances/:instanceName/disks/detach', (req, res) => { const [instance, instanceErr] = lookupInstance(req.params) if (instanceErr) return res(instanceErr) @@ -474,7 +471,7 @@ export const handlers = [ ), rest.get | GetErr>( - '/api/organizations/:orgName/projects/:projectName/instances/:instanceName/external-ips', + '/organizations/:orgName/projects/:projectName/instances/:instanceName/external-ips', (req, res) => { const [, err] = lookupInstance(req.params) if (err) return res(err) @@ -490,7 +487,7 @@ export const handlers = [ ), rest.get | GetErr>( - '/api/organizations/:orgName/projects/:projectName/instances/:instanceName/network-interfaces', + '/organizations/:orgName/projects/:projectName/instances/:instanceName/network-interfaces', (req, res) => { const [instance, err] = lookupInstance(req.params) if (err) return res(err) @@ -504,7 +501,7 @@ export const handlers = [ InstanceParams, Json | PostErr >( - '/api/organizations/:orgName/projects/:projectName/instances/:instanceName/network-interfaces', + '/organizations/:orgName/projects/:projectName/instances/:instanceName/network-interfaces', (req, res) => { const [instance, err] = lookupInstance(req.params) if (err) return res(err) @@ -553,7 +550,7 @@ export const handlers = [ ), rest.get | GetErr>( - '/api/organizations/:orgName/projects/:projectName/instances/:instanceName/network-interfaces/:interfaceName', + '/organizations/:orgName/projects/:projectName/instances/:instanceName/network-interfaces/:interfaceName', (req, res) => { const [nic, err] = lookupNetworkInterface(req.params) if (err) return res(err) @@ -566,7 +563,7 @@ export const handlers = [ NetworkInterfaceParams, Json | PostErr >( - '/api/organizations/:orgName/projects/:projectName/instances/:instanceName/network-interfaces/:interfaceName', + '/organizations/:orgName/projects/:projectName/instances/:instanceName/network-interfaces/:interfaceName', (req, res, ctx) => { const [nic, err] = lookupNetworkInterface(req.params) if (err) return res(err) @@ -593,7 +590,7 @@ export const handlers = [ ), rest.delete( - '/api/organizations/:orgName/projects/:projectName/instances/:instanceName/network-interfaces/:interfaceName', + '/organizations/:orgName/projects/:projectName/instances/:instanceName/network-interfaces/:interfaceName', (req, res, ctx) => { const [nic, err] = lookupNetworkInterface(req.params) if (err) return res(err) @@ -603,7 +600,7 @@ export const handlers = [ ), rest.get | GetErr>( - '/api/organizations/:orgName/projects/:projectName/instances/:instanceName/serial-console', + '/organizations/:orgName/projects/:projectName/instances/:instanceName/serial-console', (req, res) => { // TODO: Add support for query params return res(json(serial)) @@ -611,7 +608,7 @@ export const handlers = [ ), rest.get | GetErr>( - '/api/organizations/:orgName/projects/:projectName/disks', + '/organizations/:orgName/projects/:projectName/disks', (req, res) => { const [project, err] = lookupProject(req.params) if (err) return res(err) @@ -621,7 +618,7 @@ export const handlers = [ ), rest.post, ProjectParams, Json | PostErr>( - '/api/organizations/:orgName/projects/:projectName/disks', + '/organizations/:orgName/projects/:projectName/disks', (req, res) => { const [project, err] = lookupProject(req.params) if (err) return res(err) @@ -654,7 +651,7 @@ export const handlers = [ ), rest.delete( - '/api/organizations/:orgName/projects/:projectName/disks/:diskName', + '/organizations/:orgName/projects/:projectName/disks/:diskName', (req, res, ctx) => { const [disk, err] = lookupDisk(req.params) if (err) return res(err) @@ -673,7 +670,7 @@ export const handlers = [ ), rest.get | GetErr>( - '/api/organizations/:orgName/projects/:projectName/images', + '/organizations/:orgName/projects/:projectName/images', (req, res) => { const [project, err] = lookupProject(req.params) if (err) return res(err) @@ -683,7 +680,7 @@ export const handlers = [ ), rest.get | GetErr>( - '/api/organizations/:orgName/projects/:projectName/snapshots', + '/organizations/:orgName/projects/:projectName/snapshots', (req, res) => { const [project, err] = lookupProject(req.params) if (err) return res(err) @@ -693,7 +690,7 @@ export const handlers = [ ), rest.get | GetErr>( - '/api/organizations/:orgName/projects/:projectName/vpcs', + '/organizations/:orgName/projects/:projectName/vpcs', (req, res) => { const [project, err] = lookupProject(req.params) if (err) return res(err) @@ -703,7 +700,7 @@ export const handlers = [ ), rest.get | GetErr>( - '/api/organizations/:orgName/projects/:projectName/vpcs/:vpcName', + '/organizations/:orgName/projects/:projectName/vpcs/:vpcName', (req, res) => { const [vpc, err] = lookupVpc(req.params) if (err) return res(err) @@ -712,7 +709,7 @@ export const handlers = [ ), rest.post, ProjectParams, Json | PostErr>( - '/api/organizations/:orgName/projects/:projectName/vpcs', + '/organizations/:orgName/projects/:projectName/vpcs', (req, res) => { const [project, err] = lookupProject(req.params) if (err) return res(err) @@ -740,7 +737,7 @@ export const handlers = [ ), rest.put, VpcParams, Json | PostErr>( - '/api/organizations/:orgName/projects/:projectName/vpcs/:vpcName', + '/organizations/:orgName/projects/:projectName/vpcs/:vpcName', (req, res) => { const [vpc, err] = lookupVpc(req.params) if (err) return res(err) @@ -761,7 +758,7 @@ export const handlers = [ ), rest.get | GetErr>( - '/api/organizations/:orgName/projects/:projectName/vpcs/:vpcName/subnets', + '/organizations/:orgName/projects/:projectName/vpcs/:vpcName/subnets', (req, res) => { const [vpc, err] = lookupVpc(req.params) if (err) return res(err) @@ -771,7 +768,7 @@ export const handlers = [ ), rest.post, VpcParams, Json | PostErr>( - '/api/organizations/:orgName/projects/:projectName/vpcs/:vpcName/subnets', + '/organizations/:orgName/projects/:projectName/vpcs/:vpcName/subnets', (req, res) => { const [vpc, err] = lookupVpc(req.params) if (err) return res(err) @@ -801,7 +798,7 @@ export const handlers = [ ), rest.put, VpcSubnetParams, Json | PostErr>( - '/api/organizations/:orgName/projects/:projectName/vpcs/:vpcName/subnets/:subnetName', + '/organizations/:orgName/projects/:projectName/vpcs/:vpcName/subnets/:subnetName', (req, res, ctx) => { const [subnet, err] = lookupVpcSubnet(req.params) if (err) return res(err) @@ -817,7 +814,7 @@ export const handlers = [ ), rest.get | GetErr>( - '/api/organizations/:orgName/projects/:projectName/vpcs/:vpcName/firewall/rules', + '/organizations/:orgName/projects/:projectName/vpcs/:vpcName/firewall/rules', (req, res) => { const [vpc, err] = lookupVpc(req.params) if (err) return res(err) @@ -831,7 +828,7 @@ export const handlers = [ VpcParams, Json | PostErr >( - '/api/organizations/:orgName/projects/:projectName/vpcs/:vpcName/firewall/rules', + '/organizations/:orgName/projects/:projectName/vpcs/:vpcName/firewall/rules', (req, res) => { const [vpc, err] = lookupVpc(req.params) if (err) return res(err) @@ -851,7 +848,7 @@ export const handlers = [ ), rest.get | GetErr>( - '/api/organizations/:orgName/projects/:projectName/vpcs/:vpcName/routers', + '/organizations/:orgName/projects/:projectName/vpcs/:vpcName/routers', (req, res) => { const [vpc, err] = lookupVpc(req.params) if (err) return res(err) @@ -861,7 +858,7 @@ export const handlers = [ ), rest.post, VpcParams, Json | PostErr>( - '/api/organizations/:orgName/projects/:projectName/vpcs/:vpcName/routers', + '/organizations/:orgName/projects/:projectName/vpcs/:vpcName/routers', (req, res) => { const [vpc, err] = lookupVpc(req.params) if (err) return res(err) @@ -888,7 +885,7 @@ export const handlers = [ ), rest.put, VpcRouterParams, Json | PostErr>( - '/api/organizations/:orgName/projects/:projectName/vpcs/:vpcName/routers/:routerName', + '/organizations/:orgName/projects/:projectName/vpcs/:vpcName/routers/:routerName', (req, res, ctx) => { const [router, err] = lookupVpcRouter(req.params) if (err) return res(err) @@ -904,7 +901,7 @@ export const handlers = [ ), rest.get | GetErr>( - '/api/organizations/:orgName/projects/:projectName/vpcs/:vpcName/routers/:routerName/routes', + '/organizations/:orgName/projects/:projectName/vpcs/:vpcName/routers/:routerName/routes', (req, res) => { const [router, err] = lookupVpcRouter(req.params) if (err) return res(err) @@ -914,14 +911,14 @@ export const handlers = [ ), rest.get | GetErr>( - '/api/images', + '/images', (req, res) => { return res(json(paginated(req.url.search, db.globalImages))) } ), rest.get | GetErr>( - '/api/images/:imageName', + '/images/:imageName', (req, res) => { const [image, err] = lookupGlobalImage(req.params) if (err) return res(err) @@ -932,12 +929,12 @@ export const handlers = [ // note that in the API this is meant for system users, but that could change. // kind of a hack to pretend it's about normal users. // see https://github.com/oxidecomputer/omicron/issues/1235 - rest.get | GetErr>('/api/users', (req, res) => { + rest.get | GetErr>('/users', (req, res) => { return res(json(paginated(req.url.search, db.users))) }), rest.post, never, PostErr>( - '/api/device/confirm', + '/device/confirm', (req, res, ctx) => { if (req.body.user_code === 'BADD-CODE') { return res(ctx.status(404)) @@ -946,16 +943,23 @@ export const handlers = [ } ), - getById('/api/by-id/organizations/:id', db.orgs), - getById('/api/by-id/projects/:id', db.projects), - getById('/api/by-id/instances/:id', db.instances), - getById('/api/by-id/network-interfaces/:id', db.networkInterfaces), - getById('/api/by-id/vpcs/:id', db.vpcs), - getById('/api/by-id/vpc-subnets/:id', db.vpcSubnets), - getById('/api/by-id/vpc-routers/:id', db.vpcRouters), - getById('/api/by-id/vpc-router-routes/:id', db.vpcRouterRoutes), - getById('/api/by-id/disks/:id', db.disks), - getById('/api/by-id/global-images/:id', db.globalImages), - getById('/api/by-id/images/:id', db.images), - getById('/api/by-id/snapshots/:id', db.snapshots), -] + getById('/by-id/organizations/:id', db.orgs), + getById('/by-id/projects/:id', db.projects), + getById('/by-id/instances/:id', db.instances), + getById('/by-id/network-interfaces/:id', db.networkInterfaces), + getById('/by-id/vpcs/:id', db.vpcs), + getById('/by-id/vpc-subnets/:id', db.vpcSubnets), + getById('/by-id/vpc-routers/:id', db.vpcRouters), + getById('/by-id/vpc-router-routes/:id', db.vpcRouterRoutes), + getById('/by-id/disks/:id', db.disks), + getById('/by-id/global-images/:id', db.globalImages), + getById('/by-id/images/:id', db.images), + getById('/by-id/snapshots/:id', db.snapshots), +].map((h) => { + // Prefix if MSW is set which is not true when it's a standalone server + if (process.env.MSW) { + h.info.path = '/api' + h.info.path + return h + } + return h +}) diff --git a/package.json b/package.json index 52537e9cb6..c5c7e747c1 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "@figma-export/cli": "^4.3.0", "@figma-export/output-components-as-svgr": "^4.2.0", "@figma-export/transform-svg-with-svgo": "^4.3.0", + "@mswjs/http-middleware": "^0.5.1", "@playwright/test": "^1.25.0", "@storybook/addon-docs": "^6.5.9", "@storybook/addon-essentials": "^6.5.9", diff --git a/playwright.config.ts b/playwright.config.ts index 9fe75723a0..9fcf3ff1ac 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -14,7 +14,6 @@ const config: PlaywrightTestConfig = { /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, globalSetup: 'app/test/e2e/global-setup.ts', - globalTeardown: 'app/test/e2e/global-teardown.ts', use: { /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ baseURL: 'http://localhost:4009', diff --git a/yarn.lock b/yarn.lock index e9ffe94995..038b215a9f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2630,6 +2630,13 @@ "@types/set-cookie-parser" "^2.4.0" set-cookie-parser "^2.4.6" +"@mswjs/http-middleware@^0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@mswjs/http-middleware/-/http-middleware-0.5.1.tgz#99b1087494caee307f60a189d7a766ba50e78577" + integrity sha512-oa/p4/bG+UZ87G9rAy/Y9pEkYsm3nYI3DLtv6oM70JZLwP1QflitF+kEQDde09migH3lh+TdxSgIQ6q7061sLg== + dependencies: + express "^4.17.1" + "@mswjs/interceptors@^0.17.2": version "0.17.3" resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.17.3.tgz#9272545332c0b16ac9cae2d97bf96d3853e14969" From 847638884c362551d325a8b2999b875af952060d Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 22 Aug 2022 19:01:19 -0400 Subject: [PATCH 27/69] Fix globalSetup server logic --- app/test/e2e/global-setup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/test/e2e/global-setup.ts b/app/test/e2e/global-setup.ts index 1fcb407cc0..258e6e12f3 100644 --- a/app/test/e2e/global-setup.ts +++ b/app/test/e2e/global-setup.ts @@ -11,7 +11,7 @@ export default async function globalSetup() { expect(process.env.MSW).toBeFalsy() // If pointing to a real nexus API don't mock - if (process.env.API_URL) { + if (!process.env.API_URL) { createServer(...handlers).listen(12220) } } From de68a4b6c4a6ffafc93c312c7def54ec724e6296 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 22 Aug 2022 19:04:25 -0400 Subject: [PATCH 28/69] Address pr feedback --- libs/api-mocks/msw/db.ts | 4 ---- libs/api-mocks/msw/handlers.ts | 1 - 2 files changed, 5 deletions(-) diff --git a/libs/api-mocks/msw/db.ts b/libs/api-mocks/msw/db.ts index e515f98944..8c975ccd89 100644 --- a/libs/api-mocks/msw/db.ts +++ b/libs/api-mocks/msw/db.ts @@ -166,10 +166,6 @@ const initDb = { vpcSubnets: [mock.vpcSubnet], } -// export const db = createStore('msw-db', { -// initialValues: clone(initDb), -// }) - export let db = clone(initDb) export function resetDb() { diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index deed695aba..da9be95474 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -959,7 +959,6 @@ export const handlers = [ // Prefix if MSW is set which is not true when it's a standalone server if (process.env.MSW) { h.info.path = '/api' + h.info.path - return h } return h }) From 7edd281c3d59426433c443f014061f3ce3a3b767 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 22 Aug 2022 19:49:10 -0400 Subject: [PATCH 29/69] Fix subnet test --- app/pages/project/networking/VpcPage/VpcPage.e2e.ts | 5 ++--- libs/api-mocks/msw/handlers.ts | 13 +++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/pages/project/networking/VpcPage/VpcPage.e2e.ts b/app/pages/project/networking/VpcPage/VpcPage.e2e.ts index 9d3234db63..1da86a2fa3 100644 --- a/app/pages/project/networking/VpcPage/VpcPage.e2e.ts +++ b/app/pages/project/networking/VpcPage/VpcPage.e2e.ts @@ -26,7 +26,7 @@ test.describe('VpcPage', () => { // only one row in table, the default mock-subnet const rows = await page.locator('tbody >> tr') await expect(rows).toHaveCount(1) - await expect(rows.nth(0).locator('text="mock-subnet"')).toBeVisible() + await expect(rows.nth(0).locator(`text="default"`)).toBeVisible() // open modal, fill out form, submit await page.click('text=New subnet') @@ -36,8 +36,7 @@ test.describe('VpcPage', () => { await expect(rows).toHaveCount(2) - await expect(rows.nth(0).locator('text="mock-subnet"')).toBeVisible() - await expect(rows.nth(0).locator('text="1.1.1.1/24"')).toBeVisible() + await expect(rows.nth(0).locator('text="default"')).toBeVisible() await expect(rows.nth(1).locator('text="mock-subnet-2"')).toBeVisible() await expect(rows.nth(1).locator('text="1.1.1.2/24"')).toBeVisible() diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index da9be95474..69dd49b97d 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -732,6 +732,19 @@ export const handlers = [ ...getTimestamps(), } db.vpcs.push(newVpc) + + // Also create a default subnet + const newSubnet: Json = { + id: genId('vpc-subnet'), + name: 'default', + vpc_id: newVpc.id, + ipv6_block: 'fd2d:4569:88b1::/64', + description: '', + ipv4_block: '', + ...getTimestamps(), + } + db.vpcSubnets.push(newSubnet) + return res(json(newVpc, { status: 201 })) } ), From b46531afe15f93877c48f65e0ad1349169223fb4 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 22 Aug 2022 20:09:21 -0400 Subject: [PATCH 30/69] Patch filewall test --- app/pages/project/networking/VpcPage/VpcPage.e2e.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/pages/project/networking/VpcPage/VpcPage.e2e.ts b/app/pages/project/networking/VpcPage/VpcPage.e2e.ts index 1da86a2fa3..5b03870514 100644 --- a/app/pages/project/networking/VpcPage/VpcPage.e2e.ts +++ b/app/pages/project/networking/VpcPage/VpcPage.e2e.ts @@ -44,11 +44,10 @@ test.describe('VpcPage', () => { const defaultRules = ['allow-internal-inbound', 'allow-ssh', 'allow-icmp', 'allow-rdp'] - test('can create firewall rule', async ({ page }) => { - await page.goto('/orgs/maze-war/projects/mock-project/vpcs/mock-vpc') + test.fixme('can create firewall rule', async ({ page }) => { + await page.goto(`/orgs/${orgName}/projects/${projectName}/vpcs/${vpcName}`) await page.locator('text="Firewall Rules"').click() - // default rules are all there for (const name of defaultRules) { await expect(page.locator(`text="${name}"`)).toBeVisible() } @@ -59,7 +58,7 @@ test.describe('VpcPage', () => { await expect(modal).not.toBeVisible() // open modal - await page.locator('text="New rule"').click() + await page.locator('text="New rule"').first().click() // modal is now open await expect(modal).toBeVisible() @@ -116,8 +115,8 @@ test.describe('VpcPage', () => { } }) - test('can update firewall rule', async ({ page }) => { - await page.goto('/orgs/maze-war/projects/mock-project/vpcs/mock-vpc') + test.fixme('can update firewall rule', async ({ page }) => { + await page.goto(`/orgs/${orgName}/projects/${projectName}/vpcs/${vpcName}`) await page.locator('text="Firewall Rules"').click() const rows = await page.locator('tbody >> tr') From 2e35a0d535ed9c0c4ee106b5baf5bfab0094144f Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 22 Aug 2022 20:39:30 -0400 Subject: [PATCH 31/69] Start createInstance fixture, convert some of click evrythng --- app/pages/__tests__/click-everything.e2e.ts | 29 ++++++++++---- app/test/e2e/fixtures.ts | 43 +++++++++++++++++++++ 2 files changed, 65 insertions(+), 7 deletions(-) diff --git a/app/pages/__tests__/click-everything.e2e.ts b/app/pages/__tests__/click-everything.e2e.ts index db6b86b61b..903a953c84 100644 --- a/app/pages/__tests__/click-everything.e2e.ts +++ b/app/pages/__tests__/click-everything.e2e.ts @@ -1,15 +1,30 @@ -import { expectNotVisible, expectVisible, test } from 'app/test/e2e' +import { expectNotVisible, expectVisible, genName, test } from 'app/test/e2e' + +const orgName = genName('click-everything-org') +const projectName = genName('click-everything-proj') +const instanceName = genName('click-everything-instance') +const vpcName = genName('click-everything-vpc') + +test.beforeAll(async ({ createOrg, createProject, createVpc, createInstance }) => { + await createOrg(orgName) + await createProject(orgName, projectName) + await createInstance(orgName, projectName, instanceName) + await createVpc(orgName, projectName, vpcName) +}) -test("Click through everything and make it's all there", async ({ page }) => { - await page.goto('/orgs/maze-war/projects') +test.fixme("Click through everything and make it's all there", async ({ page }) => { + await page.goto(`/orgs/${orgName}/projects`) // Project page (instances list) - await page.click('role=link[name="mock-project"]') - await expectVisible(page, ['role=heading[name*="Instances"]', 'role=cell[name="db1"]']) + await page.click(`role=link[name="${projectName}"]`) + await expectVisible(page, [ + 'role=heading[name*="Instances"]', + `role=cell[name="${instanceName}"]`, + ]) - await page.click('role=link[name="db1"]') + await page.click(`role=link[name="${instanceName}"]`) await expectVisible(page, [ - 'role=heading[name*=db1]', + `role=heading[name=${instanceName}]`, 'role=tab[name="Storage"]', 'role=tab[name="Metrics"]', 'role=tab[name="Networking"]', diff --git a/app/test/e2e/fixtures.ts b/app/test/e2e/fixtures.ts index d46ad6ce29..e26059c503 100644 --- a/app/test/e2e/fixtures.ts +++ b/app/test/e2e/fixtures.ts @@ -2,6 +2,8 @@ import type { Page } from '@playwright/test' import { test as base } from '@playwright/test' import type { + InstanceCreate, + InstanceDeleteParams, OrganizationCreate, OrganizationDeleteParams, ProjectCreate, @@ -34,6 +36,13 @@ interface Fixtures { body?: Body ) => Promise deleteProject: (params: ProjectDeleteParams) => Promise + createInstance: ( + orgName: string, + projectName: string, + instanceName: string, + body?: Body + ) => Promise + deleteInstance: (params: InstanceDeleteParams) => Promise createVpc: ( orgName: string, projectName: string, @@ -94,6 +103,40 @@ export const test = base.extend({ }) }, + // TODO: Wire up all create options + async createInstance({ page, deleteInstance }, use) { + const instancesToRemove: InstanceDeleteParams[] = [] + await use(async (orgName, projectName, instanceName) => { + const back = await goto( + page, + `/orgs/${orgName}/projects/${projectName}/instances/new` + ) + + await page.fill('input[name=name]', instanceName) + await page.locator('.ox-radio-card').nth(3).click() + + await page.locator('input[value=ubuntu-1] ~ .ox-radio-card').click() + + await page.locator('button:has-text("Create instance")').click() + await back() + }) + + for (const params of instancesToRemove) { + await deleteInstance(params) + } + }, + + async deleteInstance({ page, deleteTableRow }, use) { + await use(async (params: InstanceDeleteParams) => { + const back = await goto( + page, + `/orgs/${params.orgName}/projects/${params.projectName}/instances` + ) + await deleteTableRow(params.instanceName) + await back() + }) + }, + async createVpc({ page, deleteVpc }, use) { const vpcsToRemove: VpcDeleteParams[] = [] From 9a4b9786a5652c46e4b817c0840117fac907c9ba Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 22 Aug 2022 21:02:58 -0400 Subject: [PATCH 32/69] Convert instance-create test --- app/forms/__tests__/instance-create.e2e.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/app/forms/__tests__/instance-create.e2e.ts b/app/forms/__tests__/instance-create.e2e.ts index d733fda32f..c9b6f68c56 100644 --- a/app/forms/__tests__/instance-create.e2e.ts +++ b/app/forms/__tests__/instance-create.e2e.ts @@ -1,8 +1,16 @@ -import { expect, expectVisible, test } from 'app/test/e2e' +import { expectVisible, genName, test } from 'app/test/e2e' test.describe('Instance Create Form', () => { + const orgName = genName('click-everything-org') + const projectName = genName('click-everything-proj') + + test.beforeEach(async ({ createOrg, createProject }) => { + await createOrg(orgName) + await createProject(orgName, projectName) + }) + test('can invoke instance create form from instances page', async ({ page }) => { - await page.goto('/orgs/maze-war/projects/mock-project/instances') + await page.goto(`/orgs/${orgName}/projects/${projectName}/instances`) await page.locator('text="New Instance"').click() await expectVisible(page, [ @@ -30,8 +38,8 @@ test.describe('Instance Create Form', () => { await page.locator('button:has-text("Create instance")').click() - await expect(page).toHaveURL( - '/orgs/maze-war/projects/mock-project/instances/mock-instance' + await page.waitForURL( + `/orgs/${orgName}/projects/${projectName}/instances/mock-instance` ) await expectVisible(page, [ From e4bfeaa7c96f79ccf572ec1bd3870a5b058f5253 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 22 Aug 2022 21:08:58 -0400 Subject: [PATCH 33/69] Convert project-create test --- app/pages/__tests__/project-create.e2e.tsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/app/pages/__tests__/project-create.e2e.tsx b/app/pages/__tests__/project-create.e2e.tsx index cf6ae2556d..d754a7fa60 100644 --- a/app/pages/__tests__/project-create.e2e.tsx +++ b/app/pages/__tests__/project-create.e2e.tsx @@ -1,8 +1,10 @@ -import { expect, expectVisible, test } from 'app/test/e2e' +import { expect, expectVisible, genName, test } from 'app/test/e2e' test.describe('Project create', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/orgs/maze-war/projects/new') + const orgName = genName('project-create-org') + test.beforeEach(async ({ page, createOrg }) => { + await createOrg(orgName) + await page.goto(`/orgs/${orgName}/projects/new`) }) test('contains expected elements', async ({ page }) => { @@ -17,7 +19,7 @@ test.describe('Project create', () => { test('navigates back to project instances page on success', async ({ page }) => { await page.fill('role=textbox[name="Name"]', 'mock-project-2') await page.click('role=button[name="Create project"]') - await expect(page).toHaveURL('/orgs/maze-war/projects/mock-project-2/instances') + await expect(page).toHaveURL(`/orgs/${orgName}/projects/mock-project-2/instances`) }) test('shows field-level validation error and does not POST', async ({ page }) => { @@ -29,8 +31,11 @@ test.describe('Project create', () => { await expectVisible(page, ['text="Must start with a lower-case letter"']) }) - test('shows form-level error for known server error', async ({ page }) => { - await page.fill('role=textbox[name="Name"]', 'mock-project') // already exists + test('shows form-level error for known server error', async ({ page, createProject }) => { + const projectName = genName('mock-project') + await createProject(orgName, projectName) + + await page.fill('role=textbox[name="Name"]', projectName) // already exists await page.click('role=button[name="Create project"]') await expectVisible(page, ['text="Project name already exists"']) }) From acbb147195774d0691696a2867dddeb8a184cb4a Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 22 Aug 2022 21:17:57 -0400 Subject: [PATCH 34/69] Remove stray pause --- app/pages/project/networking/VpcPage/VpcPage.e2e.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/app/pages/project/networking/VpcPage/VpcPage.e2e.ts b/app/pages/project/networking/VpcPage/VpcPage.e2e.ts index 5b03870514..0933ef2a47 100644 --- a/app/pages/project/networking/VpcPage/VpcPage.e2e.ts +++ b/app/pages/project/networking/VpcPage/VpcPage.e2e.ts @@ -13,7 +13,6 @@ test.describe('VpcPage', () => { test('can nav to VpcPage from /', async ({ page }) => { await page.goto('/') - await page.pause() await page.click(`table :text("${orgName}")`) await page.click(`table :text("${projectName}")`) await page.click('a:has-text("Networking")') From 4162e7500bd0df5e8dcbfea79fb6e137f32013d1 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 29 Aug 2022 15:10:51 -0400 Subject: [PATCH 35/69] Revert most tests --- app/pages/__tests__/NotFound.e2e.ts | 2 +- app/pages/__tests__/click-everything.e2e.ts | 31 ++++-------- .../__tests__/instance/attach-disk.e2e.ts | 4 +- .../__tests__/instance/networking.e2e.ts | 10 ++-- app/pages/__tests__/org-access.e2e.ts | 4 +- app/pages/__tests__/orgs.e2e.ts | 36 ++++++------- app/pages/__tests__/project-access.e2e.ts | 4 +- app/pages/__tests__/project-create.e2e.tsx | 19 +++---- app/pages/__tests__/project-selector.e2e.ts | 50 +++++++++---------- app/pages/__tests__/row-select.e2e.ts | 4 +- app/pages/__tests__/ssh-keys.e2e.ts | 4 +- .../project/networking/VpcPage/VpcPage.e2e.ts | 38 ++++++-------- 12 files changed, 93 insertions(+), 113 deletions(-) diff --git a/app/pages/__tests__/NotFound.e2e.ts b/app/pages/__tests__/NotFound.e2e.ts index 669996e8da..a02ca9eadc 100644 --- a/app/pages/__tests__/NotFound.e2e.ts +++ b/app/pages/__tests__/NotFound.e2e.ts @@ -1,4 +1,4 @@ -import { expect, test } from 'app/test/e2e' +import { expect, test } from '@playwright/test' test('Shows 404 page when a resource is not found', async ({ page }) => { await page.goto('/orgs/nonexistent') diff --git a/app/pages/__tests__/click-everything.e2e.ts b/app/pages/__tests__/click-everything.e2e.ts index 903a953c84..22b0027b2c 100644 --- a/app/pages/__tests__/click-everything.e2e.ts +++ b/app/pages/__tests__/click-everything.e2e.ts @@ -1,30 +1,17 @@ -import { expectNotVisible, expectVisible, genName, test } from 'app/test/e2e' - -const orgName = genName('click-everything-org') -const projectName = genName('click-everything-proj') -const instanceName = genName('click-everything-instance') -const vpcName = genName('click-everything-vpc') - -test.beforeAll(async ({ createOrg, createProject, createVpc, createInstance }) => { - await createOrg(orgName) - await createProject(orgName, projectName) - await createInstance(orgName, projectName, instanceName) - await createVpc(orgName, projectName, vpcName) -}) +import { test } from '@playwright/test' + +import { expectNotVisible, expectVisible } from 'app/util/e2e' -test.fixme("Click through everything and make it's all there", async ({ page }) => { - await page.goto(`/orgs/${orgName}/projects`) +test("Click through everything and make it's all there", async ({ page }) => { + await page.goto('/orgs/maze-war/projects') // Project page (instances list) - await page.click(`role=link[name="${projectName}"]`) - await expectVisible(page, [ - 'role=heading[name*="Instances"]', - `role=cell[name="${instanceName}"]`, - ]) + await page.click('role=link[name="mock-project"]') + await expectVisible(page, ['role=heading[name*="Instances"]', 'role=cell[name="db1"]']) - await page.click(`role=link[name="${instanceName}"]`) + await page.click('role=link[name="db1"]') await expectVisible(page, [ - `role=heading[name=${instanceName}]`, + 'role=heading[name*=db1]', 'role=tab[name="Storage"]', 'role=tab[name="Metrics"]', 'role=tab[name="Networking"]', diff --git a/app/pages/__tests__/instance/attach-disk.e2e.ts b/app/pages/__tests__/instance/attach-disk.e2e.ts index 2741340fe4..59a172969a 100644 --- a/app/pages/__tests__/instance/attach-disk.e2e.ts +++ b/app/pages/__tests__/instance/attach-disk.e2e.ts @@ -1,4 +1,6 @@ -import { expectVisible, test } from 'app/test/e2e' +import { test } from '@playwright/test' + +import { expectVisible } from 'app/util/e2e' import { stopInstance } from './util' diff --git a/app/pages/__tests__/instance/networking.e2e.ts b/app/pages/__tests__/instance/networking.e2e.ts index b32e3bb87b..ea2d14877b 100644 --- a/app/pages/__tests__/instance/networking.e2e.ts +++ b/app/pages/__tests__/instance/networking.e2e.ts @@ -1,10 +1,6 @@ -import { - expect, - expectNotVisible, - expectRowVisible, - expectVisible, - test, -} from 'app/test/e2e' +import { expect, test } from '@playwright/test' + +import { expectNotVisible, expectRowVisible, expectVisible } from 'app/util/e2e' import { stopInstance } from './util' diff --git a/app/pages/__tests__/org-access.e2e.ts b/app/pages/__tests__/org-access.e2e.ts index 0dc7dd8d2d..c92cb87d05 100644 --- a/app/pages/__tests__/org-access.e2e.ts +++ b/app/pages/__tests__/org-access.e2e.ts @@ -1,4 +1,6 @@ -import { expectNotVisible, expectRowVisible, expectVisible, test } from 'app/test/e2e' +import { test } from '@playwright/test' + +import { expectNotVisible, expectRowVisible, expectVisible } from 'app/util/e2e' test('Click through org access page', async ({ page }) => { await page.goto('/orgs/maze-war') diff --git a/app/pages/__tests__/orgs.e2e.ts b/app/pages/__tests__/orgs.e2e.ts index b73f356b09..287b518647 100644 --- a/app/pages/__tests__/orgs.e2e.ts +++ b/app/pages/__tests__/orgs.e2e.ts @@ -1,16 +1,17 @@ -import { expectVisible, genName, test } from 'app/test/e2e' +import { test } from '@playwright/test' -test('Root to orgs redirect', async ({ page }) => { - await page.goto('/') - await page.waitForURL('/orgs') - await expectVisible(page, ['role=heading[name="Organizations"]']) -}) +import { expectVisible } from 'app/util/e2e' -test('Create org and navigate to project', async ({ page, createOrg }) => { - await page.goto('/orgs') - await expectVisible(page, ['role=heading[name="Organizations"]']) +test('Orgs list and detail click work', async ({ page }) => { + await page.goto('/') + await expectVisible(page, [ + // note substring matcher bc headers have icons that mess with accessible name + // TODO: maybe that's bad and we should fix it in the code + 'role=heading[name*="Organizations"]', + 'role=cell[name="maze-war"]', + ]) - // verify create org form + // create org form await page.click('role=link[name="New Organization"]') await expectVisible(page, [ 'role=heading[name*="Create organization"]', @@ -20,15 +21,14 @@ test('Create org and navigate to project', async ({ page, createOrg }) => { ]) await page.goBack() - const name = genName('org-create-test') - await createOrg(name, { - description: 'used to test org creation', - }) - // org page (redirects to /org/org-name/projects) - await page.click(`role=link[name="${name}"]`) + await page.click('role=link[name="maze-war"]') await expectVisible(page, [ - 'role=heading[name="Projects"]', - 'role=heading[name="No projects"]', + 'role=heading[name*="Projects"]', + 'role=cell[name="mock-project"]', ]) + + // new project button works + await page.click('role=link[name="New Project"]') + await expectVisible(page, ['role=heading[name*="Create project"]']) }) diff --git a/app/pages/__tests__/project-access.e2e.ts b/app/pages/__tests__/project-access.e2e.ts index f60cfcbe68..626700a899 100644 --- a/app/pages/__tests__/project-access.e2e.ts +++ b/app/pages/__tests__/project-access.e2e.ts @@ -1,4 +1,6 @@ -import { expectNotVisible, expectRowVisible, expectVisible, test } from 'app/test/e2e' +import { test } from '@playwright/test' + +import { expectNotVisible, expectRowVisible, expectVisible } from 'app/util/e2e' test('Click through project access page', async ({ page }) => { await page.goto('/orgs/maze-war/projects/mock-project') diff --git a/app/pages/__tests__/project-create.e2e.tsx b/app/pages/__tests__/project-create.e2e.tsx index d754a7fa60..9013e03a67 100644 --- a/app/pages/__tests__/project-create.e2e.tsx +++ b/app/pages/__tests__/project-create.e2e.tsx @@ -1,10 +1,10 @@ -import { expect, expectVisible, genName, test } from 'app/test/e2e' +import { expect, test } from '@playwright/test' + +import { expectVisible } from 'app/util/e2e' test.describe('Project create', () => { - const orgName = genName('project-create-org') - test.beforeEach(async ({ page, createOrg }) => { - await createOrg(orgName) - await page.goto(`/orgs/${orgName}/projects/new`) + test.beforeEach(async ({ page }) => { + await page.goto('/orgs/maze-war/projects/new') }) test('contains expected elements', async ({ page }) => { @@ -19,7 +19,7 @@ test.describe('Project create', () => { test('navigates back to project instances page on success', async ({ page }) => { await page.fill('role=textbox[name="Name"]', 'mock-project-2') await page.click('role=button[name="Create project"]') - await expect(page).toHaveURL(`/orgs/${orgName}/projects/mock-project-2/instances`) + await expect(page).toHaveURL('/orgs/maze-war/projects/mock-project-2/instances') }) test('shows field-level validation error and does not POST', async ({ page }) => { @@ -31,11 +31,8 @@ test.describe('Project create', () => { await expectVisible(page, ['text="Must start with a lower-case letter"']) }) - test('shows form-level error for known server error', async ({ page, createProject }) => { - const projectName = genName('mock-project') - await createProject(orgName, projectName) - - await page.fill('role=textbox[name="Name"]', projectName) // already exists + test('shows form-level error for known server error', async ({ page }) => { + await page.fill('role=textbox[name="Name"]', 'mock-project') // already exists await page.click('role=button[name="Create project"]') await expectVisible(page, ['text="Project name already exists"']) }) diff --git a/app/pages/__tests__/project-selector.e2e.ts b/app/pages/__tests__/project-selector.e2e.ts index dece529b0a..8e2b759490 100644 --- a/app/pages/__tests__/project-selector.e2e.ts +++ b/app/pages/__tests__/project-selector.e2e.ts @@ -1,45 +1,43 @@ -import { expect, expectVisible, genName, test } from 'app/test/e2e' +import { expect, test } from '@playwright/test' -test('Project selector', async ({ page, createOrg, createProject }) => { - const orgName = genName('project-selector-org') - const p1Name = genName('project-a') - const p2Name = genName('project-b') +import { expectVisible } from 'app/util/e2e' - await createOrg(orgName) +test('Project selector', async ({ page }) => { + // create a second project + await page.goto('/orgs/maze-war/projects/new') + await page.fill('role=textbox[name="Name"]', 'other-project') + await page.click('role=button[name="Create project"]') - // Create 1st project - await createProject(orgName, p1Name) - - // Create 2nd project - await createProject(orgName, p2Name) - - // Go to the projects page - await page.goto(`/orgs/${orgName}/projects`) - - await expectVisible(page, [`role=cell[name="${p1Name}"]`, `role=cell[name="${p2Name}"]`]) + // go to projects page and make sure they're both there + await page.click('role=link[name="Projects"]') + await expect(page).toHaveURL('/orgs/maze-war/projects') + await expectVisible(page, [ + 'role=cell[name="mock-project"]', + 'role=cell[name="other-project"]', + ]) // switcher button is present, has text indicating no project selected await expect(page.locator('role=button[name="Switch project"]')).toHaveText( - `${orgName}select a project` + 'maze-warselect a project' ) await page.click('role=button[name="Switch project"]') await expectVisible(page, [ - `role=menuitem[name="${p1Name}"]`, - `role=menuitem[name="${p2Name}"]`, + 'role=menuitem[name="mock-project"]', + 'role=menuitem[name="other-project"]', ]) - // picking p1 in the menu takes you there - await page.click(`role=menuitem[name="${p1Name}"]`) - await expect(page).toHaveURL(`/orgs/${orgName}/projects/${p1Name}/instances`) + // picking mock-project in the menu takes you there + await page.click('role=menuitem[name="mock-project"]') + await expect(page).toHaveURL('/orgs/maze-war/projects/mock-project/instances') await expect(page.locator('role=button[name="Switch project"]')).toHaveText( - `${orgName}${p1Name}` + 'maze-warmock-project' ) // picking other-project in the menu takes you there await page.click('role=button[name="Switch project"]') - await page.click(`role=menuitem[name="${p2Name}"]`) - await expect(page).toHaveURL(`/orgs/${orgName}/projects/${p2Name}/instances`) + await page.click('role=menuitem[name="other-project"]') + await expect(page).toHaveURL('/orgs/maze-war/projects/other-project/instances') await expect(page.locator('role=button[name="Switch project"]')).toHaveText( - `${orgName}${p2Name}` + 'maze-warother-project' ) }) diff --git a/app/pages/__tests__/row-select.e2e.ts b/app/pages/__tests__/row-select.e2e.ts index 3af6832336..c5deac6631 100644 --- a/app/pages/__tests__/row-select.e2e.ts +++ b/app/pages/__tests__/row-select.e2e.ts @@ -1,4 +1,6 @@ -import { expect, forEach, test } from 'app/test/e2e' +import { expect, test } from '@playwright/test' + +import { forEach } from 'app/util/e2e' // This could easily be done as a testing-lib test but I want it in a real // table. The .is-selected asserts are slightly brittle (and contrary to our diff --git a/app/pages/__tests__/ssh-keys.e2e.ts b/app/pages/__tests__/ssh-keys.e2e.ts index eb39fcbfdd..ebccd913c7 100644 --- a/app/pages/__tests__/ssh-keys.e2e.ts +++ b/app/pages/__tests__/ssh-keys.e2e.ts @@ -1,4 +1,6 @@ -import { expectNotVisible, expectRowVisible, expectVisible, test } from 'app/test/e2e' +import { test } from '@playwright/test' + +import { expectNotVisible, expectRowVisible, expectVisible } from 'app/util/e2e' test('SSH keys', async ({ page }) => { await page.goto('/settings/ssh-keys') diff --git a/app/pages/project/networking/VpcPage/VpcPage.e2e.ts b/app/pages/project/networking/VpcPage/VpcPage.e2e.ts index 0933ef2a47..267e3bd984 100644 --- a/app/pages/project/networking/VpcPage/VpcPage.e2e.ts +++ b/app/pages/project/networking/VpcPage/VpcPage.e2e.ts @@ -1,31 +1,21 @@ -import { expect, genName, test } from 'app/test/e2e' +import { expect, test } from '@playwright/test' test.describe('VpcPage', () => { - const orgName = genName('vpc-page-org') - const projectName = genName('vpc-page-project') - const vpcName = genName('mock-vpc') - - test.beforeEach(async ({ createOrg, createProject, createVpc }) => { - await createOrg(orgName) - await createProject(orgName, projectName) - await createVpc(orgName, projectName, vpcName) - }) - test('can nav to VpcPage from /', async ({ page }) => { await page.goto('/') - await page.click(`table :text("${orgName}")`) - await page.click(`table :text("${projectName}")`) + await page.click('table :text("maze-war")') + await page.click('table :text("mock-project")') await page.click('a:has-text("Networking")') - await page.click(`a:has-text("${vpcName}")`) - await expect(page).toHaveURL(`/orgs/${orgName}/projects/${projectName}/vpcs/${vpcName}`) + await page.click('a:has-text("mock-vpc")') + await expect(page.locator('text=mock-subnet')).toBeVisible() }) test('can create subnet', async ({ page }) => { - await page.goto(`/orgs/${orgName}/projects/${projectName}/vpcs/${vpcName}`) + await page.goto('/orgs/maze-war/projects/mock-project/vpcs/mock-vpc') // only one row in table, the default mock-subnet const rows = await page.locator('tbody >> tr') await expect(rows).toHaveCount(1) - await expect(rows.nth(0).locator(`text="default"`)).toBeVisible() + await expect(rows.nth(0).locator('text="mock-subnet"')).toBeVisible() // open modal, fill out form, submit await page.click('text=New subnet') @@ -35,7 +25,8 @@ test.describe('VpcPage', () => { await expect(rows).toHaveCount(2) - await expect(rows.nth(0).locator('text="default"')).toBeVisible() + await expect(rows.nth(0).locator('text="mock-subnet"')).toBeVisible() + await expect(rows.nth(0).locator('text="1.1.1.1/24"')).toBeVisible() await expect(rows.nth(1).locator('text="mock-subnet-2"')).toBeVisible() await expect(rows.nth(1).locator('text="1.1.1.2/24"')).toBeVisible() @@ -43,10 +34,11 @@ test.describe('VpcPage', () => { const defaultRules = ['allow-internal-inbound', 'allow-ssh', 'allow-icmp', 'allow-rdp'] - test.fixme('can create firewall rule', async ({ page }) => { - await page.goto(`/orgs/${orgName}/projects/${projectName}/vpcs/${vpcName}`) + test('can create firewall rule', async ({ page }) => { + await page.goto('/orgs/maze-war/projects/mock-project/vpcs/mock-vpc') await page.locator('text="Firewall Rules"').click() + // default rules are all there for (const name of defaultRules) { await expect(page.locator(`text="${name}"`)).toBeVisible() } @@ -57,7 +49,7 @@ test.describe('VpcPage', () => { await expect(modal).not.toBeVisible() // open modal - await page.locator('text="New rule"').first().click() + await page.locator('text="New rule"').click() // modal is now open await expect(modal).toBeVisible() @@ -114,8 +106,8 @@ test.describe('VpcPage', () => { } }) - test.fixme('can update firewall rule', async ({ page }) => { - await page.goto(`/orgs/${orgName}/projects/${projectName}/vpcs/${vpcName}`) + test('can update firewall rule', async ({ page }) => { + await page.goto('/orgs/maze-war/projects/mock-project/vpcs/mock-vpc') await page.locator('text="Firewall Rules"').click() const rows = await page.locator('tbody >> tr') From 2b850b21bd81155dd1f3b27b8c5c31b7a74097f9 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 29 Aug 2022 15:11:21 -0400 Subject: [PATCH 36/69] Update test config to support MSW for local testing --- app/test/e2e/global-setup.ts | 8 ++------ package.json | 4 ++-- playwright.config.ts | 2 +- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/app/test/e2e/global-setup.ts b/app/test/e2e/global-setup.ts index 258e6e12f3..a58fa47d9c 100644 --- a/app/test/e2e/global-setup.ts +++ b/app/test/e2e/global-setup.ts @@ -2,16 +2,12 @@ * https://playwright.dev/docs/test-advanced#global-setup-and-teardown */ import { createServer } from '@mswjs/http-middleware' -import { expect } from '@playwright/test' import { handlers } from '@oxide/api-mocks' export default async function globalSetup() { - // e2e tests should only run with a standalone server meaning MSW should _not_ be set - expect(process.env.MSW).toBeFalsy() - - // If pointing to a real nexus API don't mock - if (!process.env.API_URL) { + // For tests not relying on mocked data and not pointing to a real server, start a local mock server + if (!process.env.MSW && !process.env.API_URL) { createServer(...handlers).listen(12220) } } diff --git a/package.json b/package.json index c5c7e747c1..a3cd47c56b 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,8 @@ "build:themes": "./tools/build_themes.sh", "ci": "yarn tsc && yarn lint && yarn test run && yarn e2e", "test": "vitest", - "e2e": "playwright test", - "e2ec": "playwright test --project=chromium", + "e2e": "MSW=1 playwright test", + "e2ec": "MSW=1 playwright test --project=chromium", "lint": "eslint --ext .js,.ts,.tsx,.json .", "fmt": "prettier --cache --write .", "gen": "plop", diff --git a/playwright.config.ts b/playwright.config.ts index 9fcf3ff1ac..de3b87d814 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -37,7 +37,7 @@ const config: PlaywrightTestConfig = { // use different port so it doesn't conflict with local dev server webServer: { - command: 'yarn start --port 4009', + command: `yarn start${process.env.MSW ? ':msw' : ''} --port 4009`, port: 4009, }, } From 2d7bdd43f760a97d4da8ec549bb0a3288cc7f6ef Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 29 Aug 2022 17:29:45 -0400 Subject: [PATCH 37/69] Fix test imports --- app/pages/__tests__/click-everything.e2e.ts | 2 +- app/pages/__tests__/instance/attach-disk.e2e.ts | 2 +- app/pages/__tests__/instance/networking.e2e.ts | 2 +- app/pages/__tests__/org-access.e2e.ts | 2 +- app/pages/__tests__/orgs.e2e.ts | 2 +- app/pages/__tests__/project-access.e2e.ts | 2 +- app/pages/__tests__/project-create.e2e.tsx | 2 +- app/pages/__tests__/project-selector.e2e.ts | 2 +- app/pages/__tests__/row-select.e2e.ts | 2 +- app/pages/__tests__/ssh-keys.e2e.ts | 2 +- app/{forms/__tests__ => test}/instance-create.e2e.ts | 0 11 files changed, 10 insertions(+), 10 deletions(-) rename app/{forms/__tests__ => test}/instance-create.e2e.ts (100%) diff --git a/app/pages/__tests__/click-everything.e2e.ts b/app/pages/__tests__/click-everything.e2e.ts index 22b0027b2c..1292677ca1 100644 --- a/app/pages/__tests__/click-everything.e2e.ts +++ b/app/pages/__tests__/click-everything.e2e.ts @@ -1,6 +1,6 @@ import { test } from '@playwright/test' -import { expectNotVisible, expectVisible } from 'app/util/e2e' +import { expectNotVisible, expectVisible } from 'app/test/e2e' test("Click through everything and make it's all there", async ({ page }) => { await page.goto('/orgs/maze-war/projects') diff --git a/app/pages/__tests__/instance/attach-disk.e2e.ts b/app/pages/__tests__/instance/attach-disk.e2e.ts index 59a172969a..9480d56f39 100644 --- a/app/pages/__tests__/instance/attach-disk.e2e.ts +++ b/app/pages/__tests__/instance/attach-disk.e2e.ts @@ -1,6 +1,6 @@ import { test } from '@playwright/test' -import { expectVisible } from 'app/util/e2e' +import { expectVisible } from 'app/test/e2e' import { stopInstance } from './util' diff --git a/app/pages/__tests__/instance/networking.e2e.ts b/app/pages/__tests__/instance/networking.e2e.ts index ea2d14877b..b8a77d34f9 100644 --- a/app/pages/__tests__/instance/networking.e2e.ts +++ b/app/pages/__tests__/instance/networking.e2e.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test' -import { expectNotVisible, expectRowVisible, expectVisible } from 'app/util/e2e' +import { expectNotVisible, expectRowVisible, expectVisible } from 'app/test/e2e' import { stopInstance } from './util' diff --git a/app/pages/__tests__/org-access.e2e.ts b/app/pages/__tests__/org-access.e2e.ts index c92cb87d05..0462cfed15 100644 --- a/app/pages/__tests__/org-access.e2e.ts +++ b/app/pages/__tests__/org-access.e2e.ts @@ -1,6 +1,6 @@ import { test } from '@playwright/test' -import { expectNotVisible, expectRowVisible, expectVisible } from 'app/util/e2e' +import { expectNotVisible, expectRowVisible, expectVisible } from 'app/test/e2e' test('Click through org access page', async ({ page }) => { await page.goto('/orgs/maze-war') diff --git a/app/pages/__tests__/orgs.e2e.ts b/app/pages/__tests__/orgs.e2e.ts index 287b518647..7a43a51fe6 100644 --- a/app/pages/__tests__/orgs.e2e.ts +++ b/app/pages/__tests__/orgs.e2e.ts @@ -1,6 +1,6 @@ import { test } from '@playwright/test' -import { expectVisible } from 'app/util/e2e' +import { expectVisible } from 'app/test/e2e' test('Orgs list and detail click work', async ({ page }) => { await page.goto('/') diff --git a/app/pages/__tests__/project-access.e2e.ts b/app/pages/__tests__/project-access.e2e.ts index 626700a899..6b67aafcf7 100644 --- a/app/pages/__tests__/project-access.e2e.ts +++ b/app/pages/__tests__/project-access.e2e.ts @@ -1,6 +1,6 @@ import { test } from '@playwright/test' -import { expectNotVisible, expectRowVisible, expectVisible } from 'app/util/e2e' +import { expectNotVisible, expectRowVisible, expectVisible } from 'app/test/e2e' test('Click through project access page', async ({ page }) => { await page.goto('/orgs/maze-war/projects/mock-project') diff --git a/app/pages/__tests__/project-create.e2e.tsx b/app/pages/__tests__/project-create.e2e.tsx index 9013e03a67..6541dcabcd 100644 --- a/app/pages/__tests__/project-create.e2e.tsx +++ b/app/pages/__tests__/project-create.e2e.tsx @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test' -import { expectVisible } from 'app/util/e2e' +import { expectVisible } from 'app/test/e2e' test.describe('Project create', () => { test.beforeEach(async ({ page }) => { diff --git a/app/pages/__tests__/project-selector.e2e.ts b/app/pages/__tests__/project-selector.e2e.ts index 8e2b759490..0deb40950c 100644 --- a/app/pages/__tests__/project-selector.e2e.ts +++ b/app/pages/__tests__/project-selector.e2e.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test' -import { expectVisible } from 'app/util/e2e' +import { expectVisible } from 'app/test/e2e' test('Project selector', async ({ page }) => { // create a second project diff --git a/app/pages/__tests__/row-select.e2e.ts b/app/pages/__tests__/row-select.e2e.ts index c5deac6631..5b0f76c906 100644 --- a/app/pages/__tests__/row-select.e2e.ts +++ b/app/pages/__tests__/row-select.e2e.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test' -import { forEach } from 'app/util/e2e' +import { forEach } from 'app/test/e2e' // This could easily be done as a testing-lib test but I want it in a real // table. The .is-selected asserts are slightly brittle (and contrary to our diff --git a/app/pages/__tests__/ssh-keys.e2e.ts b/app/pages/__tests__/ssh-keys.e2e.ts index ebccd913c7..4b2fc3aa7a 100644 --- a/app/pages/__tests__/ssh-keys.e2e.ts +++ b/app/pages/__tests__/ssh-keys.e2e.ts @@ -1,6 +1,6 @@ import { test } from '@playwright/test' -import { expectNotVisible, expectRowVisible, expectVisible } from 'app/util/e2e' +import { expectNotVisible, expectRowVisible, expectVisible } from 'app/test/e2e' test('SSH keys', async ({ page }) => { await page.goto('/settings/ssh-keys') diff --git a/app/forms/__tests__/instance-create.e2e.ts b/app/test/instance-create.e2e.ts similarity index 100% rename from app/forms/__tests__/instance-create.e2e.ts rename to app/test/instance-create.e2e.ts From 68ab5b1ab93766ecd09b2de0b1505c19c5e0de91 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 29 Aug 2022 17:33:50 -0400 Subject: [PATCH 38/69] Chunk up test config --- app/test/e2e/global-setup.ts | 2 +- package.json | 4 +-- playwright.config.ts | 48 +++++++++++++++++++++++------------- 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/app/test/e2e/global-setup.ts b/app/test/e2e/global-setup.ts index a58fa47d9c..0f5cf8f995 100644 --- a/app/test/e2e/global-setup.ts +++ b/app/test/e2e/global-setup.ts @@ -7,7 +7,7 @@ import { handlers } from '@oxide/api-mocks' export default async function globalSetup() { // For tests not relying on mocked data and not pointing to a real server, start a local mock server - if (!process.env.MSW && !process.env.API_URL) { + if (!process.env.API_URL) { createServer(...handlers).listen(12220) } } diff --git a/package.json b/package.json index a3cd47c56b..65538e3a7b 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,8 @@ "build:themes": "./tools/build_themes.sh", "ci": "yarn tsc && yarn lint && yarn test run && yarn e2e", "test": "vitest", - "e2e": "MSW=1 playwright test", - "e2ec": "MSW=1 playwright test --project=chromium", + "e2e": "playwright test", + "e2ec": "browser=Chrome playwright test", "lint": "eslint --ext .js,.ts,.tsx,.json .", "fmt": "prettier --cache --write .", "gen": "plop", diff --git a/playwright.config.ts b/playwright.config.ts index de3b87d814..ad406c6217 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -15,31 +15,45 @@ const config: PlaywrightTestConfig = { workers: process.env.CI ? 1 : undefined, globalSetup: 'app/test/e2e/global-setup.ts', use: { - /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ - baseURL: 'http://localhost:4009', trace: 'on-first-retry', }, - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, + projects: ([process.env.browser as string] ?? ['Chrome', 'Firefox', 'Safari']).flatMap( + (browser) => [ + /** + * Configuration for smoke tests, these tests don't rely on underlying mock data to work. + * Should be compatible with a live rack + */ + { + name: `smoke-${browser.toLowerCase()}`, + testMatch: [/test\/.*\.e2e\.ts/], + use: { + ...devices[`Desktop ${browser}`], + baseURL: 'http://localhost:4010', + }, + }, + { + name: `validate-${browser.toLowerCase()}`, + testMatch: [/pages\/.*\.e2e\.ts/], + use: { + ...devices[`Desktop ${browser}`], + baseURL: 'http://localhost:4010', + }, + }, + ] + ), + + // use different port so it doesn't conflict with local dev server + webServer: [ { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, + command: `yarn start:msw --port 4009`, + port: 4009, }, { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, + command: `yarn start --port 4010`, + port: 4010, }, ], - - // use different port so it doesn't conflict with local dev server - webServer: { - command: `yarn start${process.env.MSW ? ':msw' : ''} --port 4009`, - port: 4009, - }, } export default config From 7ce587db5c444782f994976f89d34e06cada4a3e Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 29 Aug 2022 17:44:58 -0400 Subject: [PATCH 39/69] Fix config logic, base url --- playwright.config.ts | 45 ++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/playwright.config.ts b/playwright.config.ts index ad406c6217..0fceb118da 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -18,30 +18,31 @@ const config: PlaywrightTestConfig = { trace: 'on-first-retry', }, - projects: ([process.env.browser as string] ?? ['Chrome', 'Firefox', 'Safari']).flatMap( - (browser) => [ - /** - * Configuration for smoke tests, these tests don't rely on underlying mock data to work. - * Should be compatible with a live rack - */ - { - name: `smoke-${browser.toLowerCase()}`, - testMatch: [/test\/.*\.e2e\.ts/], - use: { - ...devices[`Desktop ${browser}`], - baseURL: 'http://localhost:4010', - }, + projects: (process.env.browser + ? [process.env.browser] + : ['Chrome', 'Firefox', 'Safari'] + ).flatMap((browser) => [ + /** + * Configuration for smoke tests, these tests don't rely on underlying mock data to work. + * Should be compatible with a live rack + */ + { + name: `smoke-${browser.toLowerCase()}`, + testMatch: [/test\/.*\.e2e\.ts/], + use: { + ...devices[`Desktop ${browser}`], + baseURL: 'http://localhost:4010', }, - { - name: `validate-${browser.toLowerCase()}`, - testMatch: [/pages\/.*\.e2e\.ts/], - use: { - ...devices[`Desktop ${browser}`], - baseURL: 'http://localhost:4010', - }, + }, + { + name: `validate-${browser.toLowerCase()}`, + testMatch: [/pages\/.*\.e2e\.ts/], + use: { + ...devices[`Desktop ${browser}`], + baseURL: 'http://localhost:4009', }, - ] - ), + }, + ]), // use different port so it doesn't conflict with local dev server webServer: [ From 79b7e7d14cf36175b79e7463733435ba26eb48ff Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 30 Aug 2022 12:51:52 -0400 Subject: [PATCH 40/69] Split e2e out by project --- .github/workflows/lintBuildTest.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lintBuildTest.yml b/.github/workflows/lintBuildTest.yml index 97bc3c6a13..0045d07fb4 100644 --- a/.github/workflows/lintBuildTest.yml +++ b/.github/workflows/lintBuildTest.yml @@ -49,14 +49,15 @@ jobs: - name: Build for Nexus run: yarn build-for-nexus playwright: - name: Playwright (${{ matrix.shard }}/${{ strategy.job-total }}) + name: Playwright (${{ matrix.type }}-${{ matrix.browser }}) timeout-minutes: 60 runs-on: ubuntu-latest needs: install strategy: fail-fast: false matrix: - shard: [1, 2] + type: ['smoke', 'validate'] + browser: ['chrome', 'firefox', 'safari'] steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 @@ -70,4 +71,10 @@ jobs: - name: Install Playwright run: npx playwright install --with-deps - name: Run Playwright tests - run: yarn playwright test --workers=2 --shard=${{ matrix.shard }}/${{ strategy.job-total }} + run: yarn playwright test --workers=2 --project=${{matrix.type}}-${{matrix.browser}} + - uses: actions/upload-artifact@v2 + if: ${{ failure() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 7 From 80121b502fa8195a2c7f2777768dfe88bfc9c171 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 30 Aug 2022 16:43:02 -0400 Subject: [PATCH 41/69] Ensure created resources are actually cleaned up --- app/test/e2e/fixtures.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/test/e2e/fixtures.ts b/app/test/e2e/fixtures.ts index e26059c503..ab15cc9036 100644 --- a/app/test/e2e/fixtures.ts +++ b/app/test/e2e/fixtures.ts @@ -63,6 +63,7 @@ export const test = base.extend({ await page.fill('role=textbox[name="Name"]', orgName) await page.fill('role=textbox[name="Description"]', body.description || '') await page.click('role=button[name="Create organization"]') + orgsToRemove.push(orgName) await back() }) @@ -87,6 +88,8 @@ export const test = base.extend({ await page.fill('role=textbox[name="Name"]', projectName) await page.fill('role=textbox[name="Description"]', body.description || '') await page.click('role=button[name="Create project"]') + + projectsToRemove.push({ orgName, projectName }) await back() }) @@ -114,10 +117,10 @@ export const test = base.extend({ await page.fill('input[name=name]', instanceName) await page.locator('.ox-radio-card').nth(3).click() - await page.locator('input[value=ubuntu-1] ~ .ox-radio-card').click() - await page.locator('button:has-text("Create instance")').click() + + instancesToRemove.push({ orgName, projectName, instanceName }) await back() }) @@ -145,6 +148,8 @@ export const test = base.extend({ await page.fill('role=textbox[name="Name"]', vpcName) await page.fill('role=textbox[name="Description"]', body.description || '') await page.click('role=button[name="Create VPC"]') + + vpcsToRemove.push({ orgName, projectName, vpcName }) await back() }) From 7129c9e418bb5e9bfed4ae1039ee9c71c1486315 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 30 Aug 2022 16:46:38 -0400 Subject: [PATCH 42/69] Add comment to goto --- app/test/e2e/fixtures.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/test/e2e/fixtures.ts b/app/test/e2e/fixtures.ts index ab15cc9036..75344fed2e 100644 --- a/app/test/e2e/fixtures.ts +++ b/app/test/e2e/fixtures.ts @@ -14,6 +14,9 @@ import type { import { expectNotVisible } from './utils' +/** + * Returns a callback to result position and fails if response code over 400. + */ const goto = async (page: Page, url: string) => { const currentUrl = page.url() const response = await page.goto(url) From 1638587fe7cac3a9c74a98dbcf141205e3168b72 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 30 Aug 2022 16:54:11 -0400 Subject: [PATCH 43/69] Update browser env to BROWSER, lowercase expected value --- package.json | 2 +- playwright.config.ts | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index b70623168b..859ef2ba7f 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "ci": "yarn tsc && yarn lint && yarn test run && yarn e2e", "test": "vitest", "e2e": "playwright test", - "e2ec": "browser=Chrome playwright test", + "e2ec": "BROWSER=chrome playwright test", "lint": "eslint --ext .js,.ts,.tsx,.json .", "fmt": "prettier --cache --write .", "gen": "plop", diff --git a/playwright.config.ts b/playwright.config.ts index 0fceb118da..208231c1e3 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,6 +1,8 @@ import type { PlaywrightTestConfig } from '@playwright/test' import { devices } from '@playwright/test' +import { capitalize } from '@oxide/util' + /** * See https://playwright.dev/docs/test-configuration. */ @@ -18,27 +20,27 @@ const config: PlaywrightTestConfig = { trace: 'on-first-retry', }, - projects: (process.env.browser - ? [process.env.browser] - : ['Chrome', 'Firefox', 'Safari'] + projects: (process.env.BROWSER + ? [process.env.BROWSER] + : ['chrome', 'firefox', 'safari'] ).flatMap((browser) => [ /** * Configuration for smoke tests, these tests don't rely on underlying mock data to work. * Should be compatible with a live rack */ { - name: `smoke-${browser.toLowerCase()}`, + name: `smoke-${browser}`, testMatch: [/test\/.*\.e2e\.ts/], use: { - ...devices[`Desktop ${browser}`], + ...devices[`Desktop ${capitalize(browser)}`], baseURL: 'http://localhost:4010', }, }, { - name: `validate-${browser.toLowerCase()}`, + name: `validate-${browser}`, testMatch: [/pages\/.*\.e2e\.ts/], use: { - ...devices[`Desktop ${browser}`], + ...devices[`Desktop ${capitalize(browser)}`], baseURL: 'http://localhost:4009', }, }, From e4752498ce058d526ff29eb1b2c22d4e6a15372f Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 1 Sep 2022 12:37:53 -0400 Subject: [PATCH 44/69] Run only 1 vite server for e2e to avoid flakes --- app/main.tsx | 5 ++++- app/msw-mock-api.ts | 8 +++++++- app/test/unit/server.ts | 7 ++++++- libs/api-mocks/msw/handlers.ts | 8 +------- playwright.config.ts | 10 +++------- 5 files changed, 21 insertions(+), 17 deletions(-) diff --git a/app/main.tsx b/app/main.tsx index 24250d9c6f..5dce9216e4 100644 --- a/app/main.tsx +++ b/app/main.tsx @@ -36,7 +36,10 @@ function render() { ) } -if (process.env.NODE_ENV !== 'production' && process.env.MSW) { +if ( + process.env.NODE_ENV !== 'production' && + (process.env.MSW || window.navigator.userAgent.includes('MSW')) +) { // MSW has NODE_ENV !== prod built into it, but let's be extra safe // need to defer requests until after the mock server starts up startMockAPI().then(render) diff --git a/app/msw-mock-api.ts b/app/msw-mock-api.ts index acaac25ea1..8aa54a2739 100644 --- a/app/msw-mock-api.ts +++ b/app/msw-mock-api.ts @@ -57,7 +57,13 @@ export async function startMockAPI() { // Vite. ugh don't ask const { default: workerUrl } = await import('../mockServiceWorker.js?url') // https://mswjs.io/docs/api/setup-worker/start#options - await setupWorker(interceptAll, ...handlers).start({ + await setupWorker( + interceptAll, + ...handlers.map((h) => { + h.info.path = '/api' + h.info.path + return h + }) + ).start({ quiet: true, // don't log successfully handled requests serviceWorker: { url: workerUrl }, // custom handler only to make logging less noisy. unhandled requests still diff --git a/app/test/unit/server.ts b/app/test/unit/server.ts index c8157ad254..1d880aa772 100644 --- a/app/test/unit/server.ts +++ b/app/test/unit/server.ts @@ -3,7 +3,12 @@ import { setupServer } from 'msw/node' import { handlers } from '@oxide/api-mocks' -export const server = setupServer(...handlers) +export const server = setupServer( + ...handlers.map((h) => { + h.info.path = '/api' + h.info.path + return h + }) +) // Override request handlers in order to test special cases export function overrideOnce( diff --git a/libs/api-mocks/msw/handlers.ts b/libs/api-mocks/msw/handlers.ts index c9305b8f94..f00e1b3b8e 100644 --- a/libs/api-mocks/msw/handlers.ts +++ b/libs/api-mocks/msw/handlers.ts @@ -1007,10 +1007,4 @@ export const handlers = [ getById('/by-id/global-images/:id', db.globalImages), getById('/by-id/images/:id', db.images), getById('/by-id/snapshots/:id', db.snapshots), -].map((h) => { - // Append prefixes if it's running as MSW and not a standalone server - if (process.env.MSW) { - h.info.path = '/api' + h.info.path - } - return h -}) +] diff --git a/playwright.config.ts b/playwright.config.ts index 208231c1e3..0209afc7a4 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -18,6 +18,7 @@ const config: PlaywrightTestConfig = { globalSetup: 'app/test/e2e/global-setup.ts', use: { trace: 'on-first-retry', + baseURL: 'http://localhost:4009', }, projects: (process.env.BROWSER @@ -33,7 +34,6 @@ const config: PlaywrightTestConfig = { testMatch: [/test\/.*\.e2e\.ts/], use: { ...devices[`Desktop ${capitalize(browser)}`], - baseURL: 'http://localhost:4010', }, }, { @@ -41,7 +41,7 @@ const config: PlaywrightTestConfig = { testMatch: [/pages\/.*\.e2e\.ts/], use: { ...devices[`Desktop ${capitalize(browser)}`], - baseURL: 'http://localhost:4009', + userAgent: devices[`Desktop ${capitalize(browser)}`] + ' MSW', }, }, ]), @@ -49,13 +49,9 @@ const config: PlaywrightTestConfig = { // use different port so it doesn't conflict with local dev server webServer: [ { - command: `yarn start:msw --port 4009`, + command: `yarn start --port 4009`, port: 4009, }, - { - command: `yarn start --port 4010`, - port: 4010, - }, ], } From eefc736b82b72f63f951aab40a93e80bb4aec579 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 1 Sep 2022 12:43:56 -0400 Subject: [PATCH 45/69] Be a little more specific around user-agent matching --- app/main.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/main.tsx b/app/main.tsx index 5dce9216e4..1382c93691 100644 --- a/app/main.tsx +++ b/app/main.tsx @@ -38,7 +38,7 @@ function render() { if ( process.env.NODE_ENV !== 'production' && - (process.env.MSW || window.navigator.userAgent.includes('MSW')) + (process.env.MSW || window.navigator.userAgent.endsWith('MSW')) ) { // MSW has NODE_ENV !== prod built into it, but let's be extra safe // need to defer requests until after the mock server starts up From b5b196b7a76a82adbabb30f024c1b13d24bf0ad0 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Thu, 1 Sep 2022 13:03:08 -0500 Subject: [PATCH 46/69] pull out device, use .userAgent, add comment (#1136) --- playwright.config.ts | 48 +++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/playwright.config.ts b/playwright.config.ts index 0209afc7a4..8feac562e4 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -24,35 +24,33 @@ const config: PlaywrightTestConfig = { projects: (process.env.BROWSER ? [process.env.BROWSER] : ['chrome', 'firefox', 'safari'] - ).flatMap((browser) => [ - /** - * Configuration for smoke tests, these tests don't rely on underlying mock data to work. - * Should be compatible with a live rack - */ - { - name: `smoke-${browser}`, - testMatch: [/test\/.*\.e2e\.ts/], - use: { - ...devices[`Desktop ${capitalize(browser)}`], + ).flatMap((browser) => { + const device = devices[`Desktop ${capitalize(browser)}`] + return [ + /** + * Configuration for smoke tests, these tests don't rely on underlying mock data to work. + * Should be compatible with a live rack + */ + { + name: `smoke-${browser}`, + testMatch: [/test\/.*\.e2e\.ts/], + use: { ...device }, }, - }, - { - name: `validate-${browser}`, - testMatch: [/pages\/.*\.e2e\.ts/], - use: { - ...devices[`Desktop ${capitalize(browser)}`], - userAgent: devices[`Desktop ${capitalize(browser)}`] + ' MSW', + { + name: `validate-${browser}`, + testMatch: [/pages\/.*\.e2e\.ts/], + // special user agent lets us run one server that can handle both + // MSW and non-MSW requests + use: { ...device, userAgent: device.userAgent + ' MSW' }, }, - }, - ]), + ] + }), // use different port so it doesn't conflict with local dev server - webServer: [ - { - command: `yarn start --port 4009`, - port: 4009, - }, - ], + webServer: { + command: `yarn start --port 4009`, + port: 4009, + }, } export default config From a9a469627ea1533a0d5171ee08a567a64ef44439 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 1 Sep 2022 14:06:09 -0400 Subject: [PATCH 47/69] Add headers-polyfill given its a peer dep of @mswjs/http-middleware --- package.json | 1 + yarn.lock | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/package.json b/package.json index 656d988d6f..2868d95c0d 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,7 @@ "eslint-plugin-react": "^7.28.0", "eslint-plugin-react-hooks": "^4.3.0", "eslint-plugin-storybook": "^0.6.1", + "headers-polyfill": "^3.0.10", "husky": "^7.0.4", "identity-obj-proxy": "^3.0.0", "jscodeshift": "^0.13.0", diff --git a/yarn.lock b/yarn.lock index ec4d096ec0..19036dd508 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9309,6 +9309,11 @@ header-case@^2.0.4: capital-case "^1.0.4" tslib "^2.0.3" +headers-polyfill@^3.0.10: + version "3.0.10" + resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-3.0.10.tgz#51a72c0d9c32594fd23854a564c3d6c80b46b065" + integrity sha512-lOhQU7iG3AMcjmb8NIWCa+KwfJw5bY44BoWPtrj5A4iDbSD3ylGf5QcYr0ZyQnhkKQ2GgWNLdF2rfrXtXlF3nQ== + headers-polyfill@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-3.0.4.tgz#cd70c815a441dd882372fcd6eda212ce997c9b18" From 7b30b11d3edfa0fe3706bcb76b82bd9b28311dc9 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 1 Sep 2022 14:44:53 -0400 Subject: [PATCH 48/69] Retain trace on failure --- .github/workflows/lintBuildTest.yml | 2 +- playwright.config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lintBuildTest.yml b/.github/workflows/lintBuildTest.yml index 0045d07fb4..a034e7e58d 100644 --- a/.github/workflows/lintBuildTest.yml +++ b/.github/workflows/lintBuildTest.yml @@ -73,7 +73,7 @@ jobs: - name: Run Playwright tests run: yarn playwright test --workers=2 --project=${{matrix.type}}-${{matrix.browser}} - uses: actions/upload-artifact@v2 - if: ${{ failure() }} + if: always() with: name: playwright-report path: playwright-report/ diff --git a/playwright.config.ts b/playwright.config.ts index 8feac562e4..2df01b2375 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -17,7 +17,7 @@ const config: PlaywrightTestConfig = { workers: process.env.CI ? 1 : undefined, globalSetup: 'app/test/e2e/global-setup.ts', use: { - trace: 'on-first-retry', + trace: 'retain-on-failure', baseURL: 'http://localhost:4009', }, From fc08d49b2bfde30b66a6256f7d4429a7e0442de3 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 1 Sep 2022 14:52:10 -0400 Subject: [PATCH 49/69] Minor cleanup --- playwright.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright.config.ts b/playwright.config.ts index 2df01b2375..1357f1f57a 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -34,7 +34,7 @@ const config: PlaywrightTestConfig = { { name: `smoke-${browser}`, testMatch: [/test\/.*\.e2e\.ts/], - use: { ...device }, + use: device, }, { name: `validate-${browser}`, From d97873a3eef5f2b4db5dc471269124d666f6a75c Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 1 Sep 2022 17:42:21 -0400 Subject: [PATCH 50/69] If at first you don't succeed, try, try again --- playwright.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright.config.ts b/playwright.config.ts index 1357f1f57a..946eeb5ec0 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -12,7 +12,7 @@ const config: PlaywrightTestConfig = { /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, + retries: process.env.CI ? 3 : 0, /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, globalSetup: 'app/test/e2e/global-setup.ts', From 2e55806a2c18c64ad1fed7ad3fb736dbfad71630 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 1 Sep 2022 18:14:33 -0400 Subject: [PATCH 51/69] Correct the upload path for test failure results --- .github/workflows/lintBuildTest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lintBuildTest.yml b/.github/workflows/lintBuildTest.yml index a034e7e58d..044d886b8f 100644 --- a/.github/workflows/lintBuildTest.yml +++ b/.github/workflows/lintBuildTest.yml @@ -76,5 +76,5 @@ jobs: if: always() with: name: playwright-report - path: playwright-report/ + path: test-results/ retention-days: 7 From e4fddfebfed46806a5f5af9e6a7bf2bdf2a048f3 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 1 Sep 2022 23:01:06 -0400 Subject: [PATCH 52/69] Back to 2 retries --- playwright.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playwright.config.ts b/playwright.config.ts index 946eeb5ec0..1357f1f57a 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -12,7 +12,7 @@ const config: PlaywrightTestConfig = { /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* Retry on CI only */ - retries: process.env.CI ? 3 : 0, + retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, globalSetup: 'app/test/e2e/global-setup.ts', From 4b4e4c5cc830c554dd0d2732b800c13b31bf4572 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 1 Sep 2022 23:27:38 -0400 Subject: [PATCH 53/69] Store test failures under branch name --- .github/workflows/lintBuildTest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lintBuildTest.yml b/.github/workflows/lintBuildTest.yml index 044d886b8f..55c0d141f7 100644 --- a/.github/workflows/lintBuildTest.yml +++ b/.github/workflows/lintBuildTest.yml @@ -75,6 +75,6 @@ jobs: - uses: actions/upload-artifact@v2 if: always() with: - name: playwright-report + name: ${{ github.ref_name }}-e2e-failures path: test-results/ retention-days: 7 From 90f259fe2683e5ec336337e3433a1615fcaa9f6e Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 1 Sep 2022 23:35:16 -0400 Subject: [PATCH 54/69] bump vite version --- .github/workflows/lintBuildTest.yml | 6 +++++- package.json | 6 +++--- yarn.lock | 12 ------------ 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/.github/workflows/lintBuildTest.yml b/.github/workflows/lintBuildTest.yml index 55c0d141f7..e01d0bb449 100644 --- a/.github/workflows/lintBuildTest.yml +++ b/.github/workflows/lintBuildTest.yml @@ -72,9 +72,13 @@ jobs: run: npx playwright install --with-deps - name: Run Playwright tests run: yarn playwright test --workers=2 --project=${{matrix.type}}-${{matrix.browser}} + - name: Format test report name + id: test_report + run: | + echo "::set-output name=branch::$(echo ${{ github.ref_name }} | sed 's/[^0-9,a-z,A-Z]/\-/g')" - uses: actions/upload-artifact@v2 if: always() with: - name: ${{ github.ref_name }}-e2e-failures + name: ${{steps.test_report.outputs.branch}}-e2e-failures path: test-results/ retention-days: 7 diff --git a/package.json b/package.json index 645a3af607..317ed37eaf 100644 --- a/package.json +++ b/package.json @@ -70,8 +70,8 @@ "@figma-export/cli": "^4.3.0", "@figma-export/output-components-as-svgr": "^4.2.0", "@figma-export/transform-svg-with-svgo": "^4.3.0", - "@mswjs/http-middleware": "^0.5.1", "@ladle/react": "^2.4.2", + "@mswjs/http-middleware": "^0.5.1", "@playwright/test": "^1.25.0", "@testing-library/dom": "^8.11.3", "@testing-library/jest-dom": "^5.14.1", @@ -99,8 +99,8 @@ "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-react": "^7.28.0", "eslint-plugin-react-hooks": "^4.3.0", - "husky": "^7.0.4", "headers-polyfill": "^3.0.10", + "husky": "^7.0.4", "identity-obj-proxy": "^3.0.0", "jscodeshift": "^0.13.0", "jsdom": "^19.0.0", @@ -118,7 +118,7 @@ "token-transformer": "^0.0.18", "type-fest": "^2.17.0", "typescript": "4.8.2", - "vite": "^3.0.8", + "vite": "^3.0.9", "vitest": "^0.22.1", "whatwg-fetch": "^3.6.2" }, diff --git a/yarn.lock b/yarn.lock index 30e2a1616c..3658392a11 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9656,18 +9656,6 @@ vite-tsconfig-paths@^3.5.0: optionalDependencies: fsevents "~2.3.2" -vite@^3.0.8: - version "3.0.8" - resolved "https://registry.yarnpkg.com/vite/-/vite-3.0.8.tgz#aa095ad8e3e5da46d9ec7e878f262678965d6531" - integrity sha512-AOZ4eN7mrkJiOLuw8IA7piS4IdOQyQCA81GxGsAQvAZzMRi9ZwGB3TOaYsj4uLAWK46T5L4AfQ6InNGlxX30IQ== - dependencies: - esbuild "^0.14.47" - postcss "^8.4.16" - resolve "^1.22.1" - rollup ">=2.75.6 <2.77.0 || ~2.77.0" - optionalDependencies: - fsevents "~2.3.2" - vite@^3.0.9: version "3.0.9" resolved "https://registry.yarnpkg.com/vite/-/vite-3.0.9.tgz#45fac22c2a5290a970f23d66c1aef56a04be8a30" From 4143522c9166def0902e92ba2447e1d8968d6f22 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Fri, 2 Sep 2022 00:16:12 -0400 Subject: [PATCH 55/69] Always run the name formatter --- .github/workflows/lintBuildTest.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/lintBuildTest.yml b/.github/workflows/lintBuildTest.yml index e01d0bb449..df63b050c6 100644 --- a/.github/workflows/lintBuildTest.yml +++ b/.github/workflows/lintBuildTest.yml @@ -74,6 +74,7 @@ jobs: run: yarn playwright test --workers=2 --project=${{matrix.type}}-${{matrix.browser}} - name: Format test report name id: test_report + if: always() run: | echo "::set-output name=branch::$(echo ${{ github.ref_name }} | sed 's/[^0-9,a-z,A-Z]/\-/g')" - uses: actions/upload-artifact@v2 From 4bead172f47cd75cdae3936f7e96c3a353476ac4 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Fri, 2 Sep 2022 00:31:36 -0400 Subject: [PATCH 56/69] Simplify tests name for debugging script --- .github/workflows/lintBuildTest.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/lintBuildTest.yml b/.github/workflows/lintBuildTest.yml index df63b050c6..6ad9c16d6d 100644 --- a/.github/workflows/lintBuildTest.yml +++ b/.github/workflows/lintBuildTest.yml @@ -72,14 +72,9 @@ jobs: run: npx playwright install --with-deps - name: Run Playwright tests run: yarn playwright test --workers=2 --project=${{matrix.type}}-${{matrix.browser}} - - name: Format test report name - id: test_report - if: always() - run: | - echo "::set-output name=branch::$(echo ${{ github.ref_name }} | sed 's/[^0-9,a-z,A-Z]/\-/g')" - uses: actions/upload-artifact@v2 if: always() with: - name: ${{steps.test_report.outputs.branch}}-e2e-failures + name: test-results path: test-results/ retention-days: 7 From c224877b7ba0198fb4a604725962a810c6c70299 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Fri, 2 Sep 2022 00:59:00 -0400 Subject: [PATCH 57/69] Add a script for debugging when e2e tests fail on ci --- tools/debug-ci-e2e-fail.sh | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100755 tools/debug-ci-e2e-fail.sh diff --git a/tools/debug-ci-e2e-fail.sh b/tools/debug-ci-e2e-fail.sh new file mode 100755 index 0000000000..eea6e6c34d --- /dev/null +++ b/tools/debug-ci-e2e-fail.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -e +set -o pipefail + +# Get the ID of the last github actions run if there was one +RUN_ID=$(gh run list -b $(git rev-parse --abbrev-ref HEAD) -L 1 --json databaseId --jq .[0].databaseId) + +if [ -z "$RUN_ID" ]; then + echo "No action runs found for this branch" + exit 0 +fi + +if [ -d "test-results" ] && [ -f "test-results/.run" ] && [ "$RUN_ID" == "$(cat test-results/.run)" ]; then + : # Do nothing, the test results are already up to date +else + rm -rf test-results + echo "Attempting to download test failure traces for current branch..." + gh run download $RUN_ID + echo $RUN_ID > test-results/.run +fi + + +echo "Choose a test trace to view" +select test in $(ls test-results); do + npx playwright show-trace test-results/$test/trace.zip + exit 0 +done \ No newline at end of file From 1711152cf47ccc2c4ea407a154df86dd47e8d039 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Fri, 2 Sep 2022 12:06:43 -0400 Subject: [PATCH 58/69] Update instance smoke test resources to match test name --- app/test/instance-create.e2e.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/test/instance-create.e2e.ts b/app/test/instance-create.e2e.ts index c9b6f68c56..9f7bd9cac1 100644 --- a/app/test/instance-create.e2e.ts +++ b/app/test/instance-create.e2e.ts @@ -1,8 +1,8 @@ import { expectVisible, genName, test } from 'app/test/e2e' test.describe('Instance Create Form', () => { - const orgName = genName('click-everything-org') - const projectName = genName('click-everything-proj') + const orgName = genName('instance-create-org') + const projectName = genName('instance-create-proj') test.beforeEach(async ({ createOrg, createProject }) => { await createOrg(orgName) From d885a5e8124604ffe9fd6bd8d1218e440297efd4 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Fri, 2 Sep 2022 12:19:45 -0400 Subject: [PATCH 59/69] Add longer timeout for click-everything --- app/pages/__tests__/click-everything.e2e.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/pages/__tests__/click-everything.e2e.ts b/app/pages/__tests__/click-everything.e2e.ts index 1292677ca1..b030538afe 100644 --- a/app/pages/__tests__/click-everything.e2e.ts +++ b/app/pages/__tests__/click-everything.e2e.ts @@ -3,6 +3,9 @@ import { test } from '@playwright/test' import { expectNotVisible, expectVisible } from 'app/test/e2e' test("Click through everything and make it's all there", async ({ page }) => { + // TODO: This test is slow af. Let's break it down. + test.setTimeout(60000) + await page.goto('/orgs/maze-war/projects') // Project page (instances list) From 74f6c7cb6012394897e9e72b827d28e27d8cf74d Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Fri, 2 Sep 2022 12:51:56 -0400 Subject: [PATCH 60/69] Run e2e tests in prod mode --- app/main.tsx | 8 ++------ app/pages/__tests__/click-everything.e2e.ts | 3 --- app/pages/project/networking/VpcPage/VpcPage.e2e.ts | 2 +- package.json | 1 + playwright.config.ts | 2 +- vite.config.ts | 2 ++ 6 files changed, 7 insertions(+), 11 deletions(-) diff --git a/app/main.tsx b/app/main.tsx index 1382c93691..9017399e5c 100644 --- a/app/main.tsx +++ b/app/main.tsx @@ -36,12 +36,8 @@ function render() { ) } -if ( - process.env.NODE_ENV !== 'production' && - (process.env.MSW || window.navigator.userAgent.endsWith('MSW')) -) { - // MSW has NODE_ENV !== prod built into it, but let's be extra safe - // need to defer requests until after the mock server starts up +// When running E2E tests we want to allow MSW for validation tests (which set MSW in the user agent) +if (process.env.MSW || (process.env.E2E && window.navigator.userAgent.endsWith('MSW'))) { startMockAPI().then(render) } else { render() diff --git a/app/pages/__tests__/click-everything.e2e.ts b/app/pages/__tests__/click-everything.e2e.ts index b030538afe..1292677ca1 100644 --- a/app/pages/__tests__/click-everything.e2e.ts +++ b/app/pages/__tests__/click-everything.e2e.ts @@ -3,9 +3,6 @@ import { test } from '@playwright/test' import { expectNotVisible, expectVisible } from 'app/test/e2e' test("Click through everything and make it's all there", async ({ page }) => { - // TODO: This test is slow af. Let's break it down. - test.setTimeout(60000) - await page.goto('/orgs/maze-war/projects') // Project page (instances list) diff --git a/app/pages/project/networking/VpcPage/VpcPage.e2e.ts b/app/pages/project/networking/VpcPage/VpcPage.e2e.ts index 267e3bd984..2974e9dc2b 100644 --- a/app/pages/project/networking/VpcPage/VpcPage.e2e.ts +++ b/app/pages/project/networking/VpcPage/VpcPage.e2e.ts @@ -110,7 +110,7 @@ test.describe('VpcPage', () => { await page.goto('/orgs/maze-war/projects/mock-project/vpcs/mock-vpc') await page.locator('text="Firewall Rules"').click() - const rows = await page.locator('tbody >> tr') + const rows = page.locator('tbody >> tr') await expect(rows).toHaveCount(4) // allow-icmp is the one we're doing to change diff --git a/package.json b/package.json index 317ed37eaf..30cb0cade6 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "build": "vite build", "build-for-nexus": "API_URL='' vite build", "build:themes": "./tools/build_themes.sh", + "preview": "E2E=1 yarn build && vite preview", "ci": "yarn tsc && yarn lint && yarn test run && yarn e2e", "test": "vitest", "e2e": "playwright test", diff --git a/playwright.config.ts b/playwright.config.ts index 1357f1f57a..f30d504d3f 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -48,7 +48,7 @@ const config: PlaywrightTestConfig = { // use different port so it doesn't conflict with local dev server webServer: { - command: `yarn start --port 4009`, + command: `yarn preview --port 4009`, port: 4009, }, } diff --git a/vite.config.ts b/vite.config.ts index 05a948512d..22ed66b590 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -26,6 +26,8 @@ export default defineConfig(({ mode }) => ({ 'process.env.SHA': JSON.stringify(process.env.SHA), // used by MSW — number for % likelihood of API request failure (decimals allowed) 'process.env.CHAOS': JSON.stringify(mode !== 'production' && process.env.CHAOS), + // Set when app is being used by playwright tests + 'process.env.E2E': JSON.stringify(process.env.E2E), }, plugins: [ react({ From 5f506a6c7656ec1a925d5d92a77082944cb4d2d1 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Fri, 2 Sep 2022 13:35:23 -0400 Subject: [PATCH 61/69] Revert "Run e2e tests in prod mode" This reverts commit 74f6c7cb6012394897e9e72b827d28e27d8cf74d. --- app/main.tsx | 8 ++++++-- app/pages/__tests__/click-everything.e2e.ts | 3 +++ app/pages/project/networking/VpcPage/VpcPage.e2e.ts | 2 +- package.json | 1 - playwright.config.ts | 2 +- vite.config.ts | 2 -- 6 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app/main.tsx b/app/main.tsx index 9017399e5c..1382c93691 100644 --- a/app/main.tsx +++ b/app/main.tsx @@ -36,8 +36,12 @@ function render() { ) } -// When running E2E tests we want to allow MSW for validation tests (which set MSW in the user agent) -if (process.env.MSW || (process.env.E2E && window.navigator.userAgent.endsWith('MSW'))) { +if ( + process.env.NODE_ENV !== 'production' && + (process.env.MSW || window.navigator.userAgent.endsWith('MSW')) +) { + // MSW has NODE_ENV !== prod built into it, but let's be extra safe + // need to defer requests until after the mock server starts up startMockAPI().then(render) } else { render() diff --git a/app/pages/__tests__/click-everything.e2e.ts b/app/pages/__tests__/click-everything.e2e.ts index 1292677ca1..b030538afe 100644 --- a/app/pages/__tests__/click-everything.e2e.ts +++ b/app/pages/__tests__/click-everything.e2e.ts @@ -3,6 +3,9 @@ import { test } from '@playwright/test' import { expectNotVisible, expectVisible } from 'app/test/e2e' test("Click through everything and make it's all there", async ({ page }) => { + // TODO: This test is slow af. Let's break it down. + test.setTimeout(60000) + await page.goto('/orgs/maze-war/projects') // Project page (instances list) diff --git a/app/pages/project/networking/VpcPage/VpcPage.e2e.ts b/app/pages/project/networking/VpcPage/VpcPage.e2e.ts index 2974e9dc2b..267e3bd984 100644 --- a/app/pages/project/networking/VpcPage/VpcPage.e2e.ts +++ b/app/pages/project/networking/VpcPage/VpcPage.e2e.ts @@ -110,7 +110,7 @@ test.describe('VpcPage', () => { await page.goto('/orgs/maze-war/projects/mock-project/vpcs/mock-vpc') await page.locator('text="Firewall Rules"').click() - const rows = page.locator('tbody >> tr') + const rows = await page.locator('tbody >> tr') await expect(rows).toHaveCount(4) // allow-icmp is the one we're doing to change diff --git a/package.json b/package.json index 30cb0cade6..317ed37eaf 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "build": "vite build", "build-for-nexus": "API_URL='' vite build", "build:themes": "./tools/build_themes.sh", - "preview": "E2E=1 yarn build && vite preview", "ci": "yarn tsc && yarn lint && yarn test run && yarn e2e", "test": "vitest", "e2e": "playwright test", diff --git a/playwright.config.ts b/playwright.config.ts index f30d504d3f..1357f1f57a 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -48,7 +48,7 @@ const config: PlaywrightTestConfig = { // use different port so it doesn't conflict with local dev server webServer: { - command: `yarn preview --port 4009`, + command: `yarn start --port 4009`, port: 4009, }, } diff --git a/vite.config.ts b/vite.config.ts index 22ed66b590..05a948512d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -26,8 +26,6 @@ export default defineConfig(({ mode }) => ({ 'process.env.SHA': JSON.stringify(process.env.SHA), // used by MSW — number for % likelihood of API request failure (decimals allowed) 'process.env.CHAOS': JSON.stringify(mode !== 'production' && process.env.CHAOS), - // Set when app is being used by playwright tests - 'process.env.E2E': JSON.stringify(process.env.E2E), }, plugins: [ react({ From c9d145c04b5a1ae9359056adbb1d6fc51b316453 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Fri, 2 Sep 2022 13:41:37 -0400 Subject: [PATCH 62/69] Bump global timeout --- playwright.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/playwright.config.ts b/playwright.config.ts index 1357f1f57a..2069a2dfff 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -16,6 +16,7 @@ const config: PlaywrightTestConfig = { /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, globalSetup: 'app/test/e2e/global-setup.ts', + timeout: 60000, use: { trace: 'retain-on-failure', baseURL: 'http://localhost:4009', From c5602ad9ae3ed5245e59617516c76118ac049ade Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 6 Sep 2022 11:45:55 -0400 Subject: [PATCH 63/69] Update error check in project e2e --- app/pages/__tests__/project-create.e2e.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/pages/__tests__/project-create.e2e.tsx b/app/pages/__tests__/project-create.e2e.tsx index 6541dcabcd..31f830b785 100644 --- a/app/pages/__tests__/project-create.e2e.tsx +++ b/app/pages/__tests__/project-create.e2e.tsx @@ -28,7 +28,7 @@ test.describe('Project create', () => { await expect(page.locator('role=button[name="Create project"]')).toBeDisabled() await page.click('role=textbox[name="Description"]') // just to blur name input - await expectVisible(page, ['text="Must start with a lower-case letter"']) + await page.locator('text="Must start with a lower-case letter"').first().isVisible() }) test('shows form-level error for known server error', async ({ page }) => { From 47c62054213c266e7b6449d6b21eb08f48eb1740 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 6 Sep 2022 12:29:24 -0400 Subject: [PATCH 64/69] Only expect against visible elements in expectVisible --- app/pages/__tests__/project-create.e2e.tsx | 2 +- app/test/e2e/utils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/pages/__tests__/project-create.e2e.tsx b/app/pages/__tests__/project-create.e2e.tsx index 31f830b785..6541dcabcd 100644 --- a/app/pages/__tests__/project-create.e2e.tsx +++ b/app/pages/__tests__/project-create.e2e.tsx @@ -28,7 +28,7 @@ test.describe('Project create', () => { await expect(page.locator('role=button[name="Create project"]')).toBeDisabled() await page.click('role=textbox[name="Description"]') // just to blur name input - await page.locator('text="Must start with a lower-case letter"').first().isVisible() + await expectVisible(page, ['text="Must start with a lower-case letter"']) }) test('shows form-level error for known server error', async ({ page }) => { diff --git a/app/test/e2e/utils.ts b/app/test/e2e/utils.ts index fc2103148f..0735d0c6dc 100644 --- a/app/test/e2e/utils.ts +++ b/app/test/e2e/utils.ts @@ -21,7 +21,7 @@ export async function map( export async function expectVisible(page: Page, selectors: string[]) { for (const selector of selectors) { - await expect(page.locator(selector)).toBeVisible() + await expect(page.locator(selector).locator('visible=true')).toBeVisible() } } From 6ae0f7267dac9fc5cd295f4a97afc027bdf04189 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 6 Sep 2022 12:51:02 -0400 Subject: [PATCH 65/69] Update expectVisible filtering logic --- app/test/e2e/utils.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/test/e2e/utils.ts b/app/test/e2e/utils.ts index 0735d0c6dc..d2c0c4bc68 100644 --- a/app/test/e2e/utils.ts +++ b/app/test/e2e/utils.ts @@ -21,7 +21,14 @@ export async function map( export async function expectVisible(page: Page, selectors: string[]) { for (const selector of selectors) { - await expect(page.locator(selector).locator('visible=true')).toBeVisible() + /** + * We want to pass if _at least_ one element is visible matching the given + * selector. `expect(locator).toBeVisible()` will fail if more than one + * element is found. To work around this, we filter by visible and then + * select the first element. The filter is important otherwise first might + * not actually be a visible element. + */ + await expect(page.locator(selector).locator('visible=true').first()).toBeVisible() } } From f97ebc93b386de468cc8404918e29ff71613350b Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 6 Sep 2022 13:00:13 -0400 Subject: [PATCH 66/69] Fix vercel build failure --- libs/util/{classed.ts => classed.tsx} | 4 ++++ 1 file changed, 4 insertions(+) rename libs/util/{classed.ts => classed.tsx} (89%) diff --git a/libs/util/classed.ts b/libs/util/classed.tsx similarity index 89% rename from libs/util/classed.ts rename to libs/util/classed.tsx index c8427fe1a4..de842dc8e7 100644 --- a/libs/util/classed.ts +++ b/libs/util/classed.tsx @@ -1,3 +1,7 @@ +/** + * Even though this file doesn't contain JSX we've changed it to a TSX file to + * avoid build failures with the `vite:react-babel` plugin. + */ import cn from 'classnames' import React from 'react' From df5790608677c2c9d74b91a549c98b327c1d8185 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 7 Sep 2022 16:48:16 -0400 Subject: [PATCH 67/69] Add comment about userAgent flag --- app/main.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/main.tsx b/app/main.tsx index 1382c93691..46637776f2 100644 --- a/app/main.tsx +++ b/app/main.tsx @@ -36,6 +36,12 @@ function render() { ) } +/** + * The `MSW` prefix to the user agent comes from our playwright config. + * Currently it's not possible to provide different environment variables + * via test configuration so this method is used to differentiate between + * smoke tests that don't need MSW and validation tests that do. + */ if ( process.env.NODE_ENV !== 'production' && (process.env.MSW || window.navigator.userAgent.endsWith('MSW')) From f891ed29b577fec9f3b4d8b04d650dbc3aa7b2ae Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Wed, 7 Sep 2022 17:00:08 -0400 Subject: [PATCH 68/69] Update e2e info on readme --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bcdc160f74..3c07957eed 100644 --- a/README.md +++ b/README.md @@ -116,18 +116,25 @@ Using the script is strongly recommended, but if you really don't want to, make ### E2E tests with [Playwright](https://playwright.dev/) -Playwright tests match the filename pattern `.e2e.ts`. The basic command to run all tests is `yarn playwright test`. You may have to run `yarn playwright install` after `yarn install` to get the browser binaries. +Playwright tests match the filename pattern `.e2e.ts`. The basic command to run all tests is `yarn e2e`. You may have to run `yarn playwright install` after `yarn install` to get the browser binaries. + +There are two types of tests in our project. Validation tests which rely on mocked responses from MSW and smoke tests which assume a clean environment. Smoke tests are design to be ran against a rack meaning they create any required resources for the test and clean up after themselves. + +Tests are ran across `chrome`, `firefox`, and `safari` when running `yarn e2e`. Test runs can be isolated to a single browser by setting a `BROWSER` environment variable like `BROWSER=chrome yarn e2e`. Tests can be further isolated down to either smoke or validation suites by providing a `--project` argument. For example, `yarn e2e --project=validate-chrome` or `yarn e2e --project=smoke-firefox`. Some debugging tricks (see the docs [here](https://playwright.dev/docs/debug) for more details): - Add `await page.pause()` to a test and run `yarn e2e --headed --project=chromium` to run a test in a single headed browser with the excellent [Inspector](https://playwright.dev/docs/inspector) open and pause at that line. This is perfect for making sure the screen looks like you expect at that moment and testing selectors to use in the next step. +To debug end-to-end failures on CI checkout the branch with the failure and run `./tools/debug-ci-e2e-fail.sh`. It'll download the latest failures from CI and allow you to open a [playwright trace](https://playwright.dev/docs/trace-viewer-intro#viewing-the-trace) of the failure. + ### Other useful commands | Command | Description | | --------------- | ---------------------------------------------------------------------------------- | | `yarn test run` | Vitest tests | | `yarn test` | Vitest tests in watch mode | +| `yarn e2ec` | Only run end-to-end tests in chromium | | `yarn lint` | ESLint | | `yarn tsc` | Check types | | `yarn ci` | Lint, tests, and types | From 2870d4c7d6a912f7e3e8afac49cb2b7d11566c25 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 8 Sep 2022 16:43:06 -0400 Subject: [PATCH 69/69] Correct readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3c07957eed..047c919849 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ Tests are ran across `chrome`, `firefox`, and `safari` when running `yarn e2e`. Some debugging tricks (see the docs [here](https://playwright.dev/docs/debug) for more details): -- Add `await page.pause()` to a test and run `yarn e2e --headed --project=chromium` to run a test in a single headed browser with the excellent [Inspector](https://playwright.dev/docs/inspector) open and pause at that line. This is perfect for making sure the screen looks like you expect at that moment and testing selectors to use in the next step. +- Add `await page.pause()` to a test and run `BROWSER=chrome yarn e2e --headed` to run a test in a single headed browser with the excellent [Inspector](https://playwright.dev/docs/inspector) open and pause at that line. This is perfect for making sure the screen looks like you expect at that moment and testing selectors to use in the next step. To debug end-to-end failures on CI checkout the branch with the failure and run `./tools/debug-ci-e2e-fail.sh`. It'll download the latest failures from CI and allow you to open a [playwright trace](https://playwright.dev/docs/trace-viewer-intro#viewing-the-trace) of the failure.