@owlmeans/test-ui 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -0
- package/build/browser.d.ts +19 -0
- package/build/browser.d.ts.map +1 -0
- package/build/browser.js +47 -0
- package/build/browser.js.map +1 -0
- package/build/config.d.ts +15 -0
- package/build/config.d.ts.map +1 -0
- package/build/config.js +30 -0
- package/build/config.js.map +1 -0
- package/build/index.d.ts +4 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +3 -0
- package/build/index.js.map +1 -0
- package/build/mount.d.ts +40 -0
- package/build/mount.d.ts.map +1 -0
- package/build/mount.js +36 -0
- package/build/mount.js.map +1 -0
- package/harness/index.html +11 -0
- package/package.json +46 -0
- package/src/browser.ts +50 -0
- package/src/harness/.gitkeep +0 -0
- package/src/index.ts +3 -0
- package/src/mount.ts +62 -0
- package/tsconfig.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# @owlmeans/test-ui
|
|
2
|
+
|
|
3
|
+
Playwright-as-a-library helpers for **bun-test**-driven component acceptance tests of OwlMeans UI packages (`web-panel`, `web-client`, `client-panel`, `client-wl`, `web-flow`, `web-wl`, `client`, `client-i18n`).
|
|
4
|
+
|
|
5
|
+
These are **not** end-to-end UI tests — they exercise individual components in a real browser, the same way unit tests in other categories exercise individual services. The runner is `bun test` (not `bun x playwright test`), keeping the test toolchain consistent across all four categories.
|
|
6
|
+
|
|
7
|
+
## Exports
|
|
8
|
+
|
|
9
|
+
- `launchBrowser(opts?)` — return the shared chromium instance for this `bun test` process. Idempotent.
|
|
10
|
+
- `closeBrowser()` — tear it down. Call from `afterAll`.
|
|
11
|
+
- `withPage(fn)` — convenience: lease a fresh context+page for the duration of `fn`, then dispose the context.
|
|
12
|
+
- `mountComponent({ url, component?, props? })` — opens a fresh context, navigates to a harness URL with the component / props encoded, returns `{ page, close }` so the spec can assert against `page.locator(...)` and dispose the context when done.
|
|
13
|
+
- `Browser`, `BrowserContext`, `Page`, `Locator` — re-exports of the Playwright types so consumers don't need a direct `playwright` import.
|
|
14
|
+
|
|
15
|
+
## Spec shape
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { afterAll, describe, expect, test } from 'bun:test'
|
|
19
|
+
import { closeBrowser, mountComponent } from '@owlmeans/test-ui'
|
|
20
|
+
|
|
21
|
+
afterAll(async () => { await closeBrowser() })
|
|
22
|
+
|
|
23
|
+
describe('LoginForm — acceptance', () => {
|
|
24
|
+
test('renders the heading from the SKILL example', async () => {
|
|
25
|
+
const { page, close } = await mountComponent({
|
|
26
|
+
url: 'http://localhost:5173/',
|
|
27
|
+
component: 'LoginForm',
|
|
28
|
+
props: { redirect: '/' },
|
|
29
|
+
})
|
|
30
|
+
try {
|
|
31
|
+
expect(await page.locator('h1').textContent()).toBe('Sign in')
|
|
32
|
+
} finally {
|
|
33
|
+
await close()
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Component harness
|
|
40
|
+
|
|
41
|
+
`harness/index.html` is a starter page (`<div id="root"></div>` + a `<script type="module" src="/mount.tsx"></script>`). Each consuming UI package serves it via Vite (or any bundler) inside a `beforeAll` hook, registers the components under test from a per-package `mount.tsx` that reads `?component=` and `?props=` from the URL, then mounts via `createRoot(...).render(...)`.
|
|
42
|
+
|
|
43
|
+
The wiring is per-package because the React/MUI/router providers each package needs differ; see the `testing-ui` skill for the recommended layout.
|
|
44
|
+
|
|
45
|
+
## One-time setup
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
bunx playwright install chromium
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This downloads the Chromium binary used by the `playwright` library. CI installs it as a workflow step.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Browser, LaunchOptions } from 'playwright';
|
|
2
|
+
/**
|
|
3
|
+
* Launch (or return) a single shared chromium instance for the current
|
|
4
|
+
* `bun test` process. UI specs hold a fixture page off this browser; the
|
|
5
|
+
* suite calls `closeBrowser()` from `afterAll` to tear it down once.
|
|
6
|
+
*/
|
|
7
|
+
export declare const launchBrowser: (opts?: LaunchOptions) => Promise<Browser>;
|
|
8
|
+
/**
|
|
9
|
+
* Tear down the shared browser. Idempotent — safe to call from multiple
|
|
10
|
+
* `afterAll` hooks.
|
|
11
|
+
*/
|
|
12
|
+
export declare const closeBrowser: () => Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Convenience that launches the browser, runs a callback with a fresh
|
|
15
|
+
* context + page, and closes the context on completion. The browser
|
|
16
|
+
* itself stays alive across calls so multiple specs share it cheaply.
|
|
17
|
+
*/
|
|
18
|
+
export declare const withPage: <T>(fn: (page: import("playwright").Page) => Promise<T>) => Promise<T>;
|
|
19
|
+
//# sourceMappingURL=browser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAKxD;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAU,OAAM,aAAkB,KAAG,OAAO,CAAC,OAAO,CAO7E,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,YAAY,QAAa,OAAO,CAAC,IAAI,CAOjD,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,QAAQ,GAAU,CAAC,EAC9B,IAAI,CAAC,IAAI,EAAE,OAAO,YAAY,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,KAClD,OAAO,CAAC,CAAC,CASX,CAAA"}
|
package/build/browser.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { chromium } from 'playwright';
|
|
2
|
+
let shared;
|
|
3
|
+
let inflight;
|
|
4
|
+
/**
|
|
5
|
+
* Launch (or return) a single shared chromium instance for the current
|
|
6
|
+
* `bun test` process. UI specs hold a fixture page off this browser; the
|
|
7
|
+
* suite calls `closeBrowser()` from `afterAll` to tear it down once.
|
|
8
|
+
*/
|
|
9
|
+
export const launchBrowser = async (opts = {}) => {
|
|
10
|
+
if (shared != null)
|
|
11
|
+
return shared;
|
|
12
|
+
if (inflight != null)
|
|
13
|
+
return inflight;
|
|
14
|
+
inflight = chromium.launch({ headless: true, ...opts });
|
|
15
|
+
shared = await inflight;
|
|
16
|
+
inflight = undefined;
|
|
17
|
+
return shared;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Tear down the shared browser. Idempotent — safe to call from multiple
|
|
21
|
+
* `afterAll` hooks.
|
|
22
|
+
*/
|
|
23
|
+
export const closeBrowser = async () => {
|
|
24
|
+
const b = shared;
|
|
25
|
+
shared = undefined;
|
|
26
|
+
inflight = undefined;
|
|
27
|
+
if (b != null) {
|
|
28
|
+
await b.close();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Convenience that launches the browser, runs a callback with a fresh
|
|
33
|
+
* context + page, and closes the context on completion. The browser
|
|
34
|
+
* itself stays alive across calls so multiple specs share it cheaply.
|
|
35
|
+
*/
|
|
36
|
+
export const withPage = async (fn) => {
|
|
37
|
+
const browser = await launchBrowser();
|
|
38
|
+
const context = await browser.newContext();
|
|
39
|
+
const page = await context.newPage();
|
|
40
|
+
try {
|
|
41
|
+
return await fn(page);
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
await context.close();
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAGrC,IAAI,MAA2B,CAAA;AAC/B,IAAI,QAAsC,CAAA;AAE1C;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,EAAE,OAAsB,EAAE,EAAoB,EAAE;IAChF,IAAI,MAAM,IAAI,IAAI;QAAE,OAAO,MAAM,CAAA;IACjC,IAAI,QAAQ,IAAI,IAAI;QAAE,OAAO,QAAQ,CAAA;IACrC,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,CAAC,CAAA;IACvD,MAAM,GAAG,MAAM,QAAQ,CAAA;IACvB,QAAQ,GAAG,SAAS,CAAA;IACpB,OAAO,MAAM,CAAA;AACf,CAAC,CAAA;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,IAAmB,EAAE;IACpD,MAAM,CAAC,GAAG,MAAM,CAAA;IAChB,MAAM,GAAG,SAAS,CAAA;IAClB,QAAQ,GAAG,SAAS,CAAA;IACpB,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC;QACd,MAAM,CAAC,CAAC,KAAK,EAAE,CAAA;IACjB,CAAC;AACH,CAAC,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,KAAK,EAC3B,EAAmD,EACvC,EAAE;IACd,MAAM,OAAO,GAAG,MAAM,aAAa,EAAE,CAAA;IACrC,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,EAAE,CAAA;IAC1C,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAA;IACpC,IAAI,CAAC;QACH,OAAO,MAAM,EAAE,CAAC,IAAI,CAAC,CAAA;IACvB,CAAC;YAAS,CAAC;QACT,MAAM,OAAO,CAAC,KAAK,EAAE,CAAA;IACvB,CAAC;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PlaywrightTestConfig } from '@playwright/test';
|
|
2
|
+
/**
|
|
3
|
+
* Wrap `defineConfig` from `@playwright/test` with OwlMeans-wide defaults
|
|
4
|
+
* for category-D acceptance tests:
|
|
5
|
+
*
|
|
6
|
+
* - `testDir: './tests'`
|
|
7
|
+
* - chromium-only (override `projects` to add WebKit/Firefox)
|
|
8
|
+
* - retries `0` locally, `1` in CI
|
|
9
|
+
* - 10s navigation timeout
|
|
10
|
+
*
|
|
11
|
+
* Pass overrides such as `webServer`, `baseURL`, or additional `projects`
|
|
12
|
+
* directly through. Anything you set wins over the defaults.
|
|
13
|
+
*/
|
|
14
|
+
export declare const defineUiConfig: (overrides?: PlaywrightTestConfig) => PlaywrightTestConfig;
|
|
15
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AAI5D;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,cAAc,GAAI,YAAW,oBAAyB,KAAG,oBAelE,CAAA"}
|
package/build/config.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
2
|
+
const isCi = process.env.CI === 'true' || process.env.CI === '1';
|
|
3
|
+
/**
|
|
4
|
+
* Wrap `defineConfig` from `@playwright/test` with OwlMeans-wide defaults
|
|
5
|
+
* for category-D acceptance tests:
|
|
6
|
+
*
|
|
7
|
+
* - `testDir: './tests'`
|
|
8
|
+
* - chromium-only (override `projects` to add WebKit/Firefox)
|
|
9
|
+
* - retries `0` locally, `1` in CI
|
|
10
|
+
* - 10s navigation timeout
|
|
11
|
+
*
|
|
12
|
+
* Pass overrides such as `webServer`, `baseURL`, or additional `projects`
|
|
13
|
+
* directly through. Anything you set wins over the defaults.
|
|
14
|
+
*/
|
|
15
|
+
export const defineUiConfig = (overrides = {}) => defineConfig({
|
|
16
|
+
testDir: './tests',
|
|
17
|
+
fullyParallel: true,
|
|
18
|
+
forbidOnly: isCi,
|
|
19
|
+
retries: isCi ? 1 : 0,
|
|
20
|
+
reporter: isCi ? [['github'], ['list']] : 'list',
|
|
21
|
+
use: {
|
|
22
|
+
trace: 'on-first-retry',
|
|
23
|
+
...overrides.use,
|
|
24
|
+
},
|
|
25
|
+
projects: overrides.projects ?? [
|
|
26
|
+
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
|
27
|
+
],
|
|
28
|
+
...overrides,
|
|
29
|
+
});
|
|
30
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAGxD,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,EAAE,KAAK,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,EAAE,KAAK,GAAG,CAAA;AAEhE;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,YAAkC,EAAE,EAAwB,EAAE,CAC3F,YAAY,CAAC;IACX,OAAO,EAAE,SAAS;IAClB,aAAa,EAAE,IAAI;IACnB,UAAU,EAAE,IAAI;IAChB,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACrB,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM;IAChD,GAAG,EAAE;QACH,KAAK,EAAE,gBAAgB;QACvB,GAAG,SAAS,CAAC,GAAG;KACjB;IACD,QAAQ,EAAE,SAAS,CAAC,QAAQ,IAAI;QAC9B,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,gBAAgB,CAAC,EAAE,EAAE;KAC5D;IACD,GAAG,SAAS;CACb,CAAC,CAAA"}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA;AAC1B,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA"}
|
package/build/mount.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Page } from 'playwright';
|
|
2
|
+
export interface MountOptions {
|
|
3
|
+
/**
|
|
4
|
+
* URL the harness is reachable at. The consuming package is in charge
|
|
5
|
+
* of producing it — typically a Vite dev server pointed at
|
|
6
|
+
* `node_modules/@owlmeans/test-ui/harness/` plus a per-package
|
|
7
|
+
* `mount.tsx` that registers the components under test, OR an
|
|
8
|
+
* inlined `data:text/html,...` URL for the simplest smoke tests.
|
|
9
|
+
*/
|
|
10
|
+
url: string;
|
|
11
|
+
/**
|
|
12
|
+
* Component name registered in the harness. Appended as `?component=`.
|
|
13
|
+
* Omit when the harness mounts a single fixed root.
|
|
14
|
+
*/
|
|
15
|
+
component?: string;
|
|
16
|
+
/**
|
|
17
|
+
* JSON-serialisable props passed to the component. Encoded into the
|
|
18
|
+
* URL as `?props=<json>` for the harness's `mount.tsx` to read.
|
|
19
|
+
*/
|
|
20
|
+
props?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
export interface Mounted {
|
|
23
|
+
page: Page;
|
|
24
|
+
/** Closes the page's browser context — the shared browser stays alive. */
|
|
25
|
+
close: () => Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Open a fresh browser context, navigate to the harness URL with the
|
|
29
|
+
* `component` and `props` query parameters set, and return the `Page`
|
|
30
|
+
* along with a `close()` that disposes the context. Use from a `bun:test`
|
|
31
|
+
* spec to drive component-level acceptance assertions:
|
|
32
|
+
*
|
|
33
|
+
* const { page, close } = await mountComponent({ url: HARNESS, component: 'LoginForm' })
|
|
34
|
+
* try { expect(await page.locator('h1').textContent()).toBe('Sign in') }
|
|
35
|
+
* finally { await close() }
|
|
36
|
+
*
|
|
37
|
+
* `closeBrowser()` from `afterAll` tears down the shared browser at the end of the suite.
|
|
38
|
+
*/
|
|
39
|
+
export declare const mountComponent: (opts: MountOptions) => Promise<Mounted>;
|
|
40
|
+
//# sourceMappingURL=mount.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mount.d.ts","sourceRoot":"","sources":["../src/mount.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAGtC,MAAM,WAAW,YAAY;IAC3B;;;;;;OAMG;IACH,GAAG,EAAE,MAAM,CAAA;IACX;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAChC;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,IAAI,CAAA;IACV,0EAA0E;IAC1E,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3B;AAYD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,cAAc,GAAU,MAAM,YAAY,KAAG,OAAO,CAAC,OAAO,CASxE,CAAA"}
|
package/build/mount.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { launchBrowser } from './browser.js';
|
|
2
|
+
const buildUrl = (opts) => {
|
|
3
|
+
const params = new URLSearchParams();
|
|
4
|
+
if (opts.component != null)
|
|
5
|
+
params.set('component', opts.component);
|
|
6
|
+
if (opts.props != null)
|
|
7
|
+
params.set('props', JSON.stringify(opts.props));
|
|
8
|
+
const qs = params.toString();
|
|
9
|
+
if (qs === '')
|
|
10
|
+
return opts.url;
|
|
11
|
+
const sep = opts.url.includes('?') ? '&' : '?';
|
|
12
|
+
return `${opts.url}${sep}${qs}`;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Open a fresh browser context, navigate to the harness URL with the
|
|
16
|
+
* `component` and `props` query parameters set, and return the `Page`
|
|
17
|
+
* along with a `close()` that disposes the context. Use from a `bun:test`
|
|
18
|
+
* spec to drive component-level acceptance assertions:
|
|
19
|
+
*
|
|
20
|
+
* const { page, close } = await mountComponent({ url: HARNESS, component: 'LoginForm' })
|
|
21
|
+
* try { expect(await page.locator('h1').textContent()).toBe('Sign in') }
|
|
22
|
+
* finally { await close() }
|
|
23
|
+
*
|
|
24
|
+
* `closeBrowser()` from `afterAll` tears down the shared browser at the end of the suite.
|
|
25
|
+
*/
|
|
26
|
+
export const mountComponent = async (opts) => {
|
|
27
|
+
const browser = await launchBrowser();
|
|
28
|
+
const context = await browser.newContext();
|
|
29
|
+
const page = await context.newPage();
|
|
30
|
+
await page.goto(buildUrl(opts));
|
|
31
|
+
return {
|
|
32
|
+
page,
|
|
33
|
+
close: async () => { await context.close(); },
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=mount.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mount.js","sourceRoot":"","sources":["../src/mount.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AA6B5C,MAAM,QAAQ,GAAG,CAAC,IAAkB,EAAU,EAAE;IAC9C,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAA;IACpC,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI;QAAE,MAAM,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,CAAA;IACnE,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI;QAAE,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAA;IACvE,MAAM,EAAE,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAA;IAC5B,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC,GAAG,CAAA;IAC9B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAA;IAC9C,OAAO,GAAG,IAAI,CAAC,GAAG,GAAG,GAAG,GAAG,EAAE,EAAE,CAAA;AACjC,CAAC,CAAA;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EAAE,IAAkB,EAAoB,EAAE;IAC3E,MAAM,OAAO,GAAG,MAAM,aAAa,EAAE,CAAA;IACrC,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,EAAE,CAAA;IAC1C,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAA;IACpC,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAA;IAC/B,OAAO;QACL,IAAI;QACJ,KAAK,EAAE,KAAK,IAAI,EAAE,GAAG,MAAM,OAAO,CAAC,KAAK,EAAE,CAAA,CAAC,CAAC;KAC7C,CAAA;AACH,CAAC,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@owlmeans/test-ui",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc -b",
|
|
8
|
+
"dev": "nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
|
|
9
|
+
"watch": "tsc -b -w --preserveWatchOutput --pretty"
|
|
10
|
+
},
|
|
11
|
+
"main": "build/index.js",
|
|
12
|
+
"module": "build/index.js",
|
|
13
|
+
"types": "build/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"import": "./build/index.js",
|
|
17
|
+
"require": "./build/index.js",
|
|
18
|
+
"default": "./build/index.js",
|
|
19
|
+
"module": "./build/index.js",
|
|
20
|
+
"types": "./build/index.d.ts"
|
|
21
|
+
},
|
|
22
|
+
"./harness": "./harness/index.html"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@owlmeans/test": "^0.1.3",
|
|
26
|
+
"playwright": "^1.49.0"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"react": "*",
|
|
30
|
+
"react-dom": "*"
|
|
31
|
+
},
|
|
32
|
+
"peerDependenciesMeta": {
|
|
33
|
+
"react": { "optional": true },
|
|
34
|
+
"react-dom": { "optional": true }
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@owlmeans/dep-config": "^0.1.3",
|
|
38
|
+
"@types/bun": "^1.3.0",
|
|
39
|
+
"@types/react": "^19.2.7",
|
|
40
|
+
"nodemon": "^3.1.11",
|
|
41
|
+
"typescript": "^6.0.2"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/browser.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { chromium } from 'playwright'
|
|
2
|
+
import type { Browser, LaunchOptions } from 'playwright'
|
|
3
|
+
|
|
4
|
+
let shared: Browser | undefined
|
|
5
|
+
let inflight: Promise<Browser> | undefined
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Launch (or return) a single shared chromium instance for the current
|
|
9
|
+
* `bun test` process. UI specs hold a fixture page off this browser; the
|
|
10
|
+
* suite calls `closeBrowser()` from `afterAll` to tear it down once.
|
|
11
|
+
*/
|
|
12
|
+
export const launchBrowser = async (opts: LaunchOptions = {}): Promise<Browser> => {
|
|
13
|
+
if (shared != null) return shared
|
|
14
|
+
if (inflight != null) return inflight
|
|
15
|
+
inflight = chromium.launch({ headless: true, ...opts })
|
|
16
|
+
shared = await inflight
|
|
17
|
+
inflight = undefined
|
|
18
|
+
return shared
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Tear down the shared browser. Idempotent — safe to call from multiple
|
|
23
|
+
* `afterAll` hooks.
|
|
24
|
+
*/
|
|
25
|
+
export const closeBrowser = async (): Promise<void> => {
|
|
26
|
+
const b = shared
|
|
27
|
+
shared = undefined
|
|
28
|
+
inflight = undefined
|
|
29
|
+
if (b != null) {
|
|
30
|
+
await b.close()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Convenience that launches the browser, runs a callback with a fresh
|
|
36
|
+
* context + page, and closes the context on completion. The browser
|
|
37
|
+
* itself stays alive across calls so multiple specs share it cheaply.
|
|
38
|
+
*/
|
|
39
|
+
export const withPage = async <T>(
|
|
40
|
+
fn: (page: import('playwright').Page) => Promise<T>
|
|
41
|
+
): Promise<T> => {
|
|
42
|
+
const browser = await launchBrowser()
|
|
43
|
+
const context = await browser.newContext()
|
|
44
|
+
const page = await context.newPage()
|
|
45
|
+
try {
|
|
46
|
+
return await fn(page)
|
|
47
|
+
} finally {
|
|
48
|
+
await context.close()
|
|
49
|
+
}
|
|
50
|
+
}
|
|
File without changes
|
package/src/index.ts
ADDED
package/src/mount.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Page } from 'playwright'
|
|
2
|
+
import { launchBrowser } from './browser.js'
|
|
3
|
+
|
|
4
|
+
export interface MountOptions {
|
|
5
|
+
/**
|
|
6
|
+
* URL the harness is reachable at. The consuming package is in charge
|
|
7
|
+
* of producing it — typically a Vite dev server pointed at
|
|
8
|
+
* `node_modules/@owlmeans/test-ui/harness/` plus a per-package
|
|
9
|
+
* `mount.tsx` that registers the components under test, OR an
|
|
10
|
+
* inlined `data:text/html,...` URL for the simplest smoke tests.
|
|
11
|
+
*/
|
|
12
|
+
url: string
|
|
13
|
+
/**
|
|
14
|
+
* Component name registered in the harness. Appended as `?component=`.
|
|
15
|
+
* Omit when the harness mounts a single fixed root.
|
|
16
|
+
*/
|
|
17
|
+
component?: string
|
|
18
|
+
/**
|
|
19
|
+
* JSON-serialisable props passed to the component. Encoded into the
|
|
20
|
+
* URL as `?props=<json>` for the harness's `mount.tsx` to read.
|
|
21
|
+
*/
|
|
22
|
+
props?: Record<string, unknown>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface Mounted {
|
|
26
|
+
page: Page
|
|
27
|
+
/** Closes the page's browser context — the shared browser stays alive. */
|
|
28
|
+
close: () => Promise<void>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const buildUrl = (opts: MountOptions): string => {
|
|
32
|
+
const params = new URLSearchParams()
|
|
33
|
+
if (opts.component != null) params.set('component', opts.component)
|
|
34
|
+
if (opts.props != null) params.set('props', JSON.stringify(opts.props))
|
|
35
|
+
const qs = params.toString()
|
|
36
|
+
if (qs === '') return opts.url
|
|
37
|
+
const sep = opts.url.includes('?') ? '&' : '?'
|
|
38
|
+
return `${opts.url}${sep}${qs}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Open a fresh browser context, navigate to the harness URL with the
|
|
43
|
+
* `component` and `props` query parameters set, and return the `Page`
|
|
44
|
+
* along with a `close()` that disposes the context. Use from a `bun:test`
|
|
45
|
+
* spec to drive component-level acceptance assertions:
|
|
46
|
+
*
|
|
47
|
+
* const { page, close } = await mountComponent({ url: HARNESS, component: 'LoginForm' })
|
|
48
|
+
* try { expect(await page.locator('h1').textContent()).toBe('Sign in') }
|
|
49
|
+
* finally { await close() }
|
|
50
|
+
*
|
|
51
|
+
* `closeBrowser()` from `afterAll` tears down the shared browser at the end of the suite.
|
|
52
|
+
*/
|
|
53
|
+
export const mountComponent = async (opts: MountOptions): Promise<Mounted> => {
|
|
54
|
+
const browser = await launchBrowser()
|
|
55
|
+
const context = await browser.newContext()
|
|
56
|
+
const page = await context.newPage()
|
|
57
|
+
await page.goto(buildUrl(opts))
|
|
58
|
+
return {
|
|
59
|
+
page,
|
|
60
|
+
close: async () => { await context.close() },
|
|
61
|
+
}
|
|
62
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": [
|
|
3
|
+
"@owlmeans/dep-config/tsconfig.base.json",
|
|
4
|
+
"@owlmeans/dep-config/tsconfig.bun.json"
|
|
5
|
+
],
|
|
6
|
+
"compilerOptions": {
|
|
7
|
+
"rootDir": "./src/",
|
|
8
|
+
"outDir": "./build/"
|
|
9
|
+
},
|
|
10
|
+
"exclude": [
|
|
11
|
+
"./dist/**/*",
|
|
12
|
+
"./build/**/*",
|
|
13
|
+
"./harness/**/*",
|
|
14
|
+
"./*.ts"
|
|
15
|
+
]
|
|
16
|
+
}
|