@meddleware/frontend-config 0.0.0-rename.0 → 0.0.1
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/CHANGELOG.md +14 -0
- package/LICENSE +14 -0
- package/README.md +115 -0
- package/dist/a11y.d.ts +28 -0
- package/dist/axe.d.ts +12 -0
- package/dist/eslint.d.ts +33 -0
- package/dist/html-validate.d.ts +30 -0
- package/dist/index.d.ts +6 -0
- package/dist/lighthouserc.d.ts +54 -0
- package/dist/look.d.ts +35 -0
- package/dist/playwright.d.ts +21 -0
- package/dist/postcss.d.ts +5 -0
- package/dist/shots.d.ts +10 -0
- package/dist/stylelint.d.ts +105 -0
- package/dist/vite.d.ts +43 -0
- package/dist/vitest.d.ts +11 -0
- package/dist/wallet.d.ts +42 -0
- package/package.json +161 -1
- package/src/a11y.js +129 -0
- package/src/axe.js +43 -0
- package/src/browserslist-config.cjs +21 -0
- package/src/eslint.js +88 -0
- package/src/html-validate.js +49 -0
- package/src/index.js +12 -0
- package/src/lighthouserc.js +39 -0
- package/src/look.js +59 -0
- package/src/playwright.js +62 -0
- package/src/postcss.js +7 -0
- package/src/shots.js +33 -0
- package/src/stylelint.js +107 -0
- package/src/vite.js +87 -0
- package/src/vitest.js +52 -0
- package/src/wallet.js +38 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Playwright config for browser tests of a BUILT app, gallery or VitePress site. A repo's playwright.config.ts becomes
|
|
3
|
+
// import { definePlaywrightConfig } from '@meddleware/frontend-config/playwright'
|
|
4
|
+
// export default definePlaywrightConfig({ port: 4281 })
|
|
5
|
+
//
|
|
6
|
+
// Browsers: Chromium locally (fast); all five projects in CI — Chromium, Firefox, WebKit and a Pixel 7 / iPhone 14 viewport
|
|
7
|
+
// (Chromium and WebKit engines). Override with E2E_BROWSERS=chromium|all (or the `browsers` option).
|
|
8
|
+
// Screenshot capture (`npm run shots`, SHOTS=1) is for looking, not asserting: screens specs only run when asked, and then alone.
|
|
9
|
+
import { defineConfig, devices } from '@playwright/test'
|
|
10
|
+
|
|
11
|
+
/** @typedef {'chromium' | 'all'} BrowserSet */
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {object} options
|
|
15
|
+
* @param {number} options.port Preview server port (unique per repo, so repos can run side by side).
|
|
16
|
+
* @param {'app' | 'gallery' | 'vitepress'} [options.kind] What is served. `app`: `npm run build` then `vite preview`.
|
|
17
|
+
* `gallery`: `npm run build:gallery` then `vite preview --config vite.gallery.config.ts`. `vitepress`: `vitepress preview docs`
|
|
18
|
+
* (the site must already be built, so the repo's `test:e2e` script builds first).
|
|
19
|
+
* @param {string} [options.command] Full server command; replaces the one `kind` implies.
|
|
20
|
+
* @param {BrowserSet} [options.browsers] Browser set; default from E2E_BROWSERS, else `all` in CI and `chromium` otherwise.
|
|
21
|
+
* @param {string} [options.testDir]
|
|
22
|
+
* @param {import('@playwright/test').PlaywrightTestConfig} [options.extra] Merged over the defaults.
|
|
23
|
+
*/
|
|
24
|
+
export function definePlaywrightConfig({ port, kind = 'app', command, browsers, testDir = './e2e', extra = {} }) {
|
|
25
|
+
const server = {
|
|
26
|
+
app: `npm run build && npx vite preview --port ${port} --strictPort`,
|
|
27
|
+
gallery: `npm run build:gallery && npx vite preview --config vite.gallery.config.ts --port ${port} --strictPort`,
|
|
28
|
+
vitepress: `npx vitepress preview docs --port ${port}`,
|
|
29
|
+
}[kind]
|
|
30
|
+
const requested = browsers ?? process.env.E2E_BROWSERS ?? (process.env.CI && !process.env.SHOTS ? 'all' : 'chromium')
|
|
31
|
+
const all = requested === 'all'
|
|
32
|
+
const projects = [
|
|
33
|
+
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
|
34
|
+
...(all
|
|
35
|
+
? [
|
|
36
|
+
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
|
|
37
|
+
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
|
|
38
|
+
{ name: 'mobile-chrome', use: { ...devices['Pixel 7'] } },
|
|
39
|
+
{ name: 'mobile-safari', use: { ...devices['iPhone 14'] } },
|
|
40
|
+
]
|
|
41
|
+
: []),
|
|
42
|
+
]
|
|
43
|
+
return defineConfig({
|
|
44
|
+
testDir,
|
|
45
|
+
// A screenshot run loads only the screens specs; a normal run never loads them. (Tests are attributed to the file that calls
|
|
46
|
+
// `test()`, which for `defineShots` is shots.js, so a `playwright test e2e/screens.spec.ts` file filter would find none.)
|
|
47
|
+
...(process.env.SHOTS ? { testMatch: '**/screens.spec.ts' } : { testIgnore: ['**/screens.spec.ts'] }),
|
|
48
|
+
fullyParallel: true,
|
|
49
|
+
forbidOnly: !!process.env.CI,
|
|
50
|
+
retries: process.env.CI ? 1 : 0,
|
|
51
|
+
reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list',
|
|
52
|
+
use: { baseURL: `http://localhost:${port}`, viewport: { width: 1280, height: 900 }, trace: 'retain-on-failure' },
|
|
53
|
+
webServer: {
|
|
54
|
+
command: command ?? server,
|
|
55
|
+
url: `http://localhost:${port}/`,
|
|
56
|
+
reuseExistingServer: !process.env.CI,
|
|
57
|
+
timeout: 180_000,
|
|
58
|
+
},
|
|
59
|
+
projects,
|
|
60
|
+
...extra,
|
|
61
|
+
})
|
|
62
|
+
}
|
package/src/postcss.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Autoprefixer over the CSS a build ships, targeting the repo's browserslist (which should extend
|
|
3
|
+
// `@meddleware/frontend-config/browserslist-config`). A repo's postcss.config.js becomes
|
|
4
|
+
// export { default } from '@meddleware/frontend-config/postcss'
|
|
5
|
+
import autoprefixer from 'autoprefixer'
|
|
6
|
+
|
|
7
|
+
export default { plugins: [autoprefixer()] }
|
package/src/shots.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Screenshot matrix for LOOKING at the UI (not assertions): one PNG per state × viewport × theme × season, written to SHOTS_OUT
|
|
3
|
+
// (default test-results/shots). Excluded from `npm run test:e2e` (definePlaywrightConfig ignores screens specs unless SHOTS is
|
|
4
|
+
// set); nothing is compared against a baseline (baselines drift across machines and fonts). Compare runs by eye: shoot before
|
|
5
|
+
// and after a change and read both sets.
|
|
6
|
+
import { test } from '@playwright/test'
|
|
7
|
+
import { VIEWPORTS, applyLook, looks, offline } from './look.js'
|
|
8
|
+
|
|
9
|
+
/** @typedef {(page: import('@playwright/test').Page) => Promise<void>} State */
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Register one test per state × look. Call once, at the top level of an `e2e/screens.spec.ts`.
|
|
13
|
+
* @param {Record<string, State>} states
|
|
14
|
+
* @param {{ out?: string }} [options]
|
|
15
|
+
*/
|
|
16
|
+
export function defineShots(states, options = {}) {
|
|
17
|
+
const out = options.out ?? process.env.SHOTS_OUT ?? 'test-results/shots'
|
|
18
|
+
for (const { viewport, theme, season } of looks()) {
|
|
19
|
+
for (const [name, go] of Object.entries(states)) {
|
|
20
|
+
test(`${name} · ${viewport} · ${theme} · ${season}`, async ({ page }) => {
|
|
21
|
+
await page.setViewportSize(VIEWPORTS[viewport])
|
|
22
|
+
await page.emulateMedia({ colorScheme: /** @type {'light' | 'dark'} */ (theme), reducedMotion: 'reduce' })
|
|
23
|
+
await offline(page)
|
|
24
|
+
await go(page)
|
|
25
|
+
await applyLook(page, theme, season)
|
|
26
|
+
// chromium keeps the plain name; other browsers get a suffix so runs never overwrite each other
|
|
27
|
+
const project = test.info().project.name
|
|
28
|
+
const tag = project === 'chromium' ? '' : `.${project}`
|
|
29
|
+
await page.screenshot({ path: `${out}/${name}.${viewport}.${theme}.${season}${tag}.png`, fullPage: true })
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/stylelint.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Stylelint over CSS and the <style> blocks of Vue SFCs: the standard config tuned for a token-driven codebase, no hex colours,
|
|
3
|
+
// token-only values for colour and layering, accessibility rules (@double-great/stylelint-a11y) and browser-support linting
|
|
4
|
+
// against the shared browserslist (stylelint-no-unsupported-browser-features). A repo's stylelint.config.js becomes
|
|
5
|
+
// import { stylelintConfig } from '@meddleware/frontend-config/stylelint'
|
|
6
|
+
// export default stylelintConfig()
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
import postcssHtml from 'postcss-html'
|
|
9
|
+
|
|
10
|
+
// Resolve plugins and the base config from THIS package, not the consumer's directory, so they are found however the consumer's
|
|
11
|
+
// package manager lays out node_modules (stylelint otherwise resolves names relative to the consumer's config file).
|
|
12
|
+
/** @param {string} name */
|
|
13
|
+
const here = (name) => fileURLToPath(import.meta.resolve(name))
|
|
14
|
+
|
|
15
|
+
/** Colour-bearing properties: a literal colour here bypasses the design tokens (and so the seasons). */
|
|
16
|
+
const COLOUR_PROPERTIES = ['/color$/', 'fill', 'stroke', 'outline-color', 'text-decoration-color', 'caret-color', 'accent-color']
|
|
17
|
+
|
|
18
|
+
/** Values that carry no colour of their own, so they are allowed where a token is expected. */
|
|
19
|
+
const NEUTRAL_VALUES = ['transparent', 'inherit', 'currentColor', 'currentcolor', 'none', 'initial', 'unset', 'revert', 'auto', '0']
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {object} [options]
|
|
23
|
+
* @param {'error' | 'warning' | false} [options.tokens] Severity of the token-only rule. Default `error`; a repo with
|
|
24
|
+
* literals still to fix starts at `warning` and moves to `error` in the same release as the fixes.
|
|
25
|
+
* @param {'error' | 'warning'} [options.compat] Severity of unsupported-browser-feature findings. Default `error`.
|
|
26
|
+
* @param {string[]} [options.browsers] Browsers to check against. Default: the repo's browserslist (the shared support promise).
|
|
27
|
+
* @param {Record<string, unknown>} [options.rules] Rule overrides.
|
|
28
|
+
*/
|
|
29
|
+
export function stylelintConfig({ tokens = 'error', compat = 'error', browsers, rules = {} } = {}) {
|
|
30
|
+
return {
|
|
31
|
+
extends: [here('stylelint-config-standard')],
|
|
32
|
+
plugins: [
|
|
33
|
+
here('stylelint-no-unsupported-browser-features'),
|
|
34
|
+
here('@double-great/stylelint-a11y'),
|
|
35
|
+
here('stylelint-declaration-strict-value'),
|
|
36
|
+
],
|
|
37
|
+
overrides: [{ files: ['**/*.vue', '**/*.html'], customSyntax: postcssHtml }],
|
|
38
|
+
rules: {
|
|
39
|
+
// The design system leans heavily on custom properties and deliberate ordering; these standard rules are too noisy for
|
|
40
|
+
// a token-driven codebase.
|
|
41
|
+
'custom-property-empty-line-before': null,
|
|
42
|
+
'declaration-empty-line-before': null,
|
|
43
|
+
'comment-empty-line-before': null,
|
|
44
|
+
'rule-empty-line-before': null,
|
|
45
|
+
'at-rule-empty-line-before': null,
|
|
46
|
+
'no-descending-specificity': null,
|
|
47
|
+
// BEM-style class names (block__element--modifier) are intentional.
|
|
48
|
+
'selector-class-pattern': null,
|
|
49
|
+
// Some presentational SFCs use compact single-line multi-declaration rules.
|
|
50
|
+
'declaration-block-single-line-max-declarations': null,
|
|
51
|
+
// Preserve camelCase CSS keyword values (for example text-rendering: optimizeLegibility).
|
|
52
|
+
'value-keyword-case': null,
|
|
53
|
+
// Vue SFC scoped-style pseudo-classes.
|
|
54
|
+
'selector-pseudo-class-no-unknown': [true, { ignorePseudoClasses: ['deep', 'slotted', 'global'] }],
|
|
55
|
+
// Private panel vars use a leading-underscore convention (--_bg, --_text).
|
|
56
|
+
'custom-property-pattern': null,
|
|
57
|
+
// Vendor-prefixed properties are emitted by autoprefixer at build time; the hand-written -webkit-/-moz-
|
|
58
|
+
// font-smoothing pair is intentional.
|
|
59
|
+
'property-no-vendor-prefix': null,
|
|
60
|
+
'value-no-vendor-prefix': null,
|
|
61
|
+
|
|
62
|
+
// Colours come from the design tokens, never a literal.
|
|
63
|
+
'color-no-hex': true,
|
|
64
|
+
|
|
65
|
+
...(tokens
|
|
66
|
+
? {
|
|
67
|
+
'scale-unlimited/declaration-strict-value': [
|
|
68
|
+
[...COLOUR_PROPERTIES, 'z-index'],
|
|
69
|
+
{ ignoreValues: NEUTRAL_VALUES, disableFix: true, severity: tokens },
|
|
70
|
+
],
|
|
71
|
+
}
|
|
72
|
+
: {}),
|
|
73
|
+
|
|
74
|
+
// Browser support against the shared browserslist. Partial support is accepted: the feature degrades, it does not break.
|
|
75
|
+
// Three more degrade harmlessly and are ignored on purpose: `resize` (touch Safari has no resizable fields), non-standard
|
|
76
|
+
// `cursor` values (a pointer-device nicety), and the extended system font keywords (every use has a fallback stack).
|
|
77
|
+
'plugin/no-unsupported-browser-features': [
|
|
78
|
+
true,
|
|
79
|
+
{
|
|
80
|
+
severity: compat,
|
|
81
|
+
ignorePartialSupport: true,
|
|
82
|
+
ignore: ['css-resize', 'css3-cursors', 'extended-system-fonts'],
|
|
83
|
+
...(browsers ? { browsers } : {}),
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
|
|
87
|
+
// Accessibility.
|
|
88
|
+
// (`a11y/media-prefers-reduced-motion` is off: @meddleware/ui's reset applies one global reduced-motion rule to every app,
|
|
89
|
+
// so a per-rule media query is redundant noise.)
|
|
90
|
+
'a11y/media-prefers-reduced-motion': null,
|
|
91
|
+
'a11y/no-outline-none': true,
|
|
92
|
+
// (`a11y/selector-pseudo-class-focus` is off: it asks for a `:focus` twin of every `:hover`, but the reset gives every focusable
|
|
93
|
+
// element a visible `:focus-visible` ring and `a11y/no-outline-none` below stops anything removing it, so a hover-only cosmetic
|
|
94
|
+
// needs no twin.)
|
|
95
|
+
'a11y/selector-pseudo-class-focus': null,
|
|
96
|
+
'a11y/content-property-no-static-value': true,
|
|
97
|
+
'a11y/font-size-is-readable': true,
|
|
98
|
+
'a11y/no-obsolete-attribute': true,
|
|
99
|
+
'a11y/no-obsolete-element': true,
|
|
100
|
+
'a11y/no-text-align-justify': true,
|
|
101
|
+
|
|
102
|
+
...rules,
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export default stylelintConfig()
|
package/src/vite.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Vite build target derived from the repo's browserslist, so the shipped JS and CSS are transformed for exactly the browsers the
|
|
3
|
+
// support promise names (instead of `esnext`, or Vite's own default). In vite.config.ts:
|
|
4
|
+
// import { viteTarget } from '@meddleware/frontend-config/vite'
|
|
5
|
+
// export default defineConfig({ build: { target: viteTarget() } })
|
|
6
|
+
import { readFileSync } from 'node:fs'
|
|
7
|
+
import { createRequire } from 'node:module'
|
|
8
|
+
import { join } from 'node:path'
|
|
9
|
+
import browserslistToEsbuild from 'browserslist-to-esbuild'
|
|
10
|
+
|
|
11
|
+
/** @param {string | string[]} [queries] Browserslist queries; default: the repo's own configuration. */
|
|
12
|
+
export function viteTarget(queries) {
|
|
13
|
+
return browserslistToEsbuild(queries)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Resolve a package file from the app's own directory, where `@meddleware/ui` and the tokens are installed. */
|
|
17
|
+
function resolveFromApp(/** @type {string} */ specifier) {
|
|
18
|
+
return createRequire(join(process.cwd(), 'noop.js')).resolve(specifier)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The colour mode and the browser chrome, before the first paint: a Vite plugin for an app's `index.html`.
|
|
23
|
+
*
|
|
24
|
+
* - Serves `@meddleware/ui`'s `theme-init.js` at `/theme-init.js` (dev) and emits it into the build, and loads it as the first,
|
|
25
|
+
* render-blocking script in `<head>`. It applies the saved (or default) colour mode and the season to `<html>` before anything
|
|
26
|
+
* paints, so a page never flashes the wrong theme while its JavaScript loads. It is a same-origin file, so the CSP needs no hash
|
|
27
|
+
* and no `'unsafe-inline'`.
|
|
28
|
+
* - Adds `<meta name="color-scheme" content="light dark">` (native controls and scrollbars follow the theme) and the browser-chrome
|
|
29
|
+
* `theme-color` for each colour mode, from the design tokens' page background.
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* plugins: [vue(), themeInit({ default: 'dark' })]
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* @param {object} [options]
|
|
36
|
+
* @param {'light' | 'dark' | 'system'} [options.default] The mode a first visit gets (a saved choice always wins). Default `system`.
|
|
37
|
+
* @param {string} [options.scriptPath] The script to serve (default: `@meddleware/ui/theme-init.js` as installed in the app).
|
|
38
|
+
* @param {{ light: string, dark: string }} [options.colors] The `theme-color` values (default: the tokens' page backgrounds).
|
|
39
|
+
* @returns {import('vite').Plugin}
|
|
40
|
+
*/
|
|
41
|
+
export function themeInit({ default: mode = 'system', scriptPath, colors } = {}) {
|
|
42
|
+
const read = () => readFileSync(scriptPath ?? resolveFromApp('@meddleware/ui/theme-init.js'), 'utf8')
|
|
43
|
+
const chrome = () => {
|
|
44
|
+
if (colors) return colors
|
|
45
|
+
const { semantic } = JSON.parse(readFileSync(resolveFromApp('@meddleware/design-tokens/tokens.json'), 'utf8'))
|
|
46
|
+
return { light: semantic.light.bg, dark: semantic.dark.bg }
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
name: 'meddleware:theme-init',
|
|
50
|
+
configureServer(server) {
|
|
51
|
+
server.middlewares.use('/theme-init.js', (_req, res) => {
|
|
52
|
+
res.setHeader('content-type', 'text/javascript')
|
|
53
|
+
res.end(read())
|
|
54
|
+
})
|
|
55
|
+
},
|
|
56
|
+
generateBundle() {
|
|
57
|
+
this.emitFile({ type: 'asset', fileName: 'theme-init.js', source: read() })
|
|
58
|
+
},
|
|
59
|
+
transformIndexHtml() {
|
|
60
|
+
const { light, dark } = chrome()
|
|
61
|
+
return [
|
|
62
|
+
{ tag: 'script', attrs: { src: '/theme-init.js', 'data-default': mode }, injectTo: 'head-prepend' },
|
|
63
|
+
{ tag: 'meta', attrs: { name: 'color-scheme', content: 'light dark' }, injectTo: 'head' },
|
|
64
|
+
{ tag: 'meta', attrs: { name: 'theme-color', content: light, media: '(prefers-color-scheme: light)' }, injectTo: 'head' },
|
|
65
|
+
{ tag: 'meta', attrs: { name: 'theme-color', content: dark, media: '(prefers-color-scheme: dark)' }, injectTo: 'head' },
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The end-to-end build mode registers a mock wallet and a stub chain client in the app (`@meddleware/wallet-adapter/testing`), which must
|
|
73
|
+
* never reach a production build. The app's entry loads it only when `import.meta.env.MODE === 'e2e' && import.meta.env.VITE_E2E === '1'`
|
|
74
|
+
* (both static, so every other build drops the block); this makes `VITE_E2E=1` outside `--mode e2e` a build error instead of a silent no-op.
|
|
75
|
+
*
|
|
76
|
+
* ```ts
|
|
77
|
+
* export default defineConfig(({ mode }) => { assertE2eOnlyInE2eMode(mode, process.env); return { … } })
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* @param {string} mode Vite's mode.
|
|
81
|
+
* @param {Record<string, string | undefined>} env The environment (default: the process's).
|
|
82
|
+
*/
|
|
83
|
+
export function assertE2eOnlyInE2eMode(mode, env = process.env) {
|
|
84
|
+
if (env.VITE_E2E && mode !== 'e2e') {
|
|
85
|
+
throw new Error(`VITE_E2E is set but the build mode is "${mode}": the end-to-end build is \`vite build --mode e2e\`, and nothing else may include the test wallet.`)
|
|
86
|
+
}
|
|
87
|
+
}
|
package/src/vitest.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Vitest settings for a repo whose code imports the Meddleware UI libraries. Those packages ship ES modules that import their own `.css`
|
|
3
|
+
// files (each component brings its CSS), and Node cannot import a `.css` file. Vitest runs `node_modules` dependencies through Node by
|
|
4
|
+
// default, so the import fails: the libraries have to go through Vite instead, which also gives them the SAME copy of Vue as the code
|
|
5
|
+
// under test (a second copy breaks reactivity across the boundary, which is why tests used to mock `@meddleware/ui`).
|
|
6
|
+
//
|
|
7
|
+
// // vitest.config.ts (and, in a jsdom test that renders a dialog: beforeAll(installDialogShim))
|
|
8
|
+
// import { inlineMeddleware } from '@meddleware/frontend-config/vitest'
|
|
9
|
+
// export default defineConfig({
|
|
10
|
+
// plugins: [vue()],
|
|
11
|
+
// resolve: { dedupe: ['vue', '@vue/test-utils'] },
|
|
12
|
+
// test: { server: { deps: { inline: inlineMeddleware } } },
|
|
13
|
+
// })
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Packages to process through Vite in tests: the ones that import CSS or `.vue` files. Vitest matches these against the module's resolved
|
|
17
|
+
* file path (`…/node_modules/@meddleware/ui/dist/…`), not the package name, so the patterns match the path.
|
|
18
|
+
*/
|
|
19
|
+
export const inlineMeddleware = [/[\\/]@meddleware[\\/](ui|wallet-adapter|walrus-storage-kit|design-tokens)[\\/]/]
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* jsdom does not implement `<dialog>` modality (`showModal`, `close`, `returnValue`). This minimal shim models the parts `UiDialog` relies on:
|
|
23
|
+
* the `open` attribute, `returnValue`, and a `close` event on closing. Call it once (`beforeAll(installDialogShim)`) in a jsdom test that
|
|
24
|
+
* renders a dialog; the browser's real behaviour (top layer, inert background) is checked by the Playwright suites, not jsdom.
|
|
25
|
+
*/
|
|
26
|
+
export function installDialogShim() {
|
|
27
|
+
if (typeof HTMLDialogElement === 'undefined') return // a node-environment test file: there is no DOM to patch
|
|
28
|
+
const proto = /** @type {HTMLDialogElement & { _rv?: string }} */ (HTMLDialogElement.prototype)
|
|
29
|
+
if (typeof proto.showModal === 'function') return
|
|
30
|
+
if (!('returnValue' in proto)) {
|
|
31
|
+
Object.defineProperty(proto, 'returnValue', {
|
|
32
|
+
configurable: true,
|
|
33
|
+
get: /** @this {{ _rv?: string }} */ function () {
|
|
34
|
+
return this._rv ?? ''
|
|
35
|
+
},
|
|
36
|
+
set: /** @this {{ _rv?: string }} */ function (/** @type {string} */ v) {
|
|
37
|
+
this._rv = v
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
proto.showModal = /** @this {HTMLDialogElement} */ function () {
|
|
42
|
+
this.returnValue = ''
|
|
43
|
+
this.setAttribute('open', '')
|
|
44
|
+
}
|
|
45
|
+
proto.show = proto.showModal
|
|
46
|
+
proto.close = /** @this {HTMLDialogElement} */ function (returnValue) {
|
|
47
|
+
if (!this.hasAttribute('open')) return
|
|
48
|
+
if (returnValue !== undefined) this.returnValue = returnValue
|
|
49
|
+
this.removeAttribute('open')
|
|
50
|
+
this.dispatchEvent(new Event('close'))
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/wallet.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Playwright side of the shared mock wallet (`@meddleware/wallet-adapter/testing`, registered by an app's e2e build): connect it
|
|
3
|
+
// through the app's own wallet dialog, or give it a signer that lives in Node, so a key never enters the page.
|
|
4
|
+
|
|
5
|
+
/** The address the mock wallet connects with (`MOCK_ADDRESS` in wallet-adapter's testing module). */
|
|
6
|
+
export const E2E_ADDRESS = `0x${'a'.repeat(64)}`
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Connect the mock wallet as a visitor would: open the wallet dialog, choose the wallet, and wait for the dialog to close.
|
|
10
|
+
* @param {import('@playwright/test').Page} page
|
|
11
|
+
* @param {{ wallet?: string, trigger?: string | RegExp }} [options] The wallet's name in the picker; the name of the button that opens the dialog.
|
|
12
|
+
*/
|
|
13
|
+
export async function connectMockWallet(page, { wallet = 'Test Wallet', trigger = 'Connect wallet' } = {}) {
|
|
14
|
+
await page.getByRole('button', { name: trigger }).first().click()
|
|
15
|
+
const dialog = page.getByRole('dialog')
|
|
16
|
+
await dialog.getByRole('button', { name: wallet }).click()
|
|
17
|
+
await dialog.waitFor({ state: 'hidden' })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {object} NodeSigner
|
|
22
|
+
* @property {(transactionJson: string, chain: string) => Promise<{ bytes: string, signature: string }>} [signTransaction]
|
|
23
|
+
* Build and sign a transaction (`Transaction.from(json)`); resolves base64 bytes and signature.
|
|
24
|
+
* @property {(message: string) => Promise<{ signature: string }>} [signPersonalMessage] Sign a personal message (base64 bytes).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Let the page's mock wallet sign in this Node process. Call it before `page.goto`: the functions are exposed to every page load, and
|
|
29
|
+
* `installE2eWallet` uses them when they exist.
|
|
30
|
+
* @param {import('@playwright/test').Page} page
|
|
31
|
+
* @param {NodeSigner} signer
|
|
32
|
+
* @param {{ address?: string }} [options] The address the wallet connects with: the signer's own, so the transaction's sender and its signature agree.
|
|
33
|
+
*/
|
|
34
|
+
export async function exposeSigner(page, signer, { address } = {}) {
|
|
35
|
+
if (address) await page.addInitScript((a) => Object.assign(window, { __e2eWalletAddress: a }), address)
|
|
36
|
+
if (signer.signTransaction) await page.exposeFunction('__e2eSignTransaction', signer.signTransaction)
|
|
37
|
+
if (signer.signPersonalMessage) await page.exposeFunction('__e2eSignPersonalMessage', signer.signPersonalMessage)
|
|
38
|
+
}
|