The control beside the guide, the attempt line, the run badges, and the failure lists name a registry by its name alone; only the option in the list carries the host it names.
109 lines
7.7 KiB
TypeScript
109 lines
7.7 KiB
TypeScript
// A real Host/Remote/browser composition with a controllable package-manager process:
|
||
// the dialog offers the configured registries, the check asks them in turn while one
|
||
// is unreachable, and an install that loses its registry mid-run moves on to the next.
|
||
import { mkdtemp, readFile, writeFile, rm } from 'node:fs/promises'
|
||
import { tmpdir } from 'node:os'
|
||
import { join } from 'node:path'
|
||
import { fileURLToPath } from 'node:url'
|
||
import { chromium } from 'playwright'
|
||
import { expect, it } from 'vitest'
|
||
import { launchWebScaffold, captureStableAria, compareOrRefreshGolden, webSnapshotMode, watchConsole, type WebScaffold } from './scaffold.ts'
|
||
import { ZH_BROWSER_LOCALE } from './support.ts'
|
||
|
||
const MIRROR = 'https://registry.npmmirror.com/'
|
||
|
||
it('offers the registries, remembers the one picked, and moves an install on to the next when a run loses its registry', async () => {
|
||
const scratch = await mkdtemp(join(tmpdir(), 'dsh-install-registry-'))
|
||
const overlay = join(scratch, 'cordis.patch.yml')
|
||
await writeFile(overlay, `- id: plugin-manager\n config: ${JSON.stringify({ pnpmCommand: process.execPath })}\n`)
|
||
let scaffold: WebScaffold | undefined
|
||
try {
|
||
scaffold = await launchWebScaffold({ profile: { packages: [] }, extraOverlayPath: overlay })
|
||
const browser = await chromium.launch()
|
||
try {
|
||
const profile = join(scaffold.harnessHome, 'profiles', 'scaffold')
|
||
const manifestPath = join(profile, 'package.json')
|
||
// Node stands in for pnpm. `config` names npm's own registry as pnpm's own, so the mirror is a fallback for it.
|
||
// `view` answers only when asked at the mirror: pnpm's own registry times out, the way an unreachable
|
||
// registry.npmjs.org does, and pnpm prints the refusal as JSON on stdout. `add` fails its first run wherever
|
||
// it is asked, naming the registry, and installs the package on the next; the Host moves it on to the next registry.
|
||
await writeFile(join(profile, 'config'), 'console.log("https://registry.npmjs.org/")\n')
|
||
await writeFile(join(profile, 'view'), `
|
||
if (!process.argv.includes('--registry=${MIRROR}')) {
|
||
console.log(JSON.stringify({ error: { code: 'ERR_PNPM_META_FETCH_FAIL', message: 'GET https://registry.npmjs.org/mirrored-package: request timed out (ETIMEDOUT)' } }));
|
||
process.exitCode = 1;
|
||
} else {
|
||
console.log(JSON.stringify({ name: 'mirrored-package', version: '2.0.0', dsh: { bundle: { patch: './cordis.patch.yml' } } }));
|
||
}
|
||
`)
|
||
await writeFile(join(profile, 'add'), `
|
||
import('node:fs').then(fs => {
|
||
if (!fs.existsSync('.first-run')) {
|
||
fs.writeFileSync('.first-run', '');
|
||
fs.writeFileSync('pnpm-lock.yaml', 'partial lockfile');
|
||
console.error('ERR_PNPM_META_FETCH_FAIL GET https://registry.npmmirror.com/mirrored-package: socket hang up');
|
||
process.exitCode = 1;
|
||
return;
|
||
}
|
||
fs.mkdirSync('node_modules/mirrored-package', { recursive: true });
|
||
fs.writeFileSync('node_modules/mirrored-package/package.json', JSON.stringify({ name: 'mirrored-package', version: '2.0.0', dsh: { bundle: { patch: './cordis.patch.yml' } } }));
|
||
fs.writeFileSync('node_modules/mirrored-package/cordis.patch.yml', '[]\\n');
|
||
fs.writeFileSync('package.json', JSON.stringify({ ...JSON.parse(fs.readFileSync('package.json', 'utf8')), dependencies: { 'mirrored-package': '2.0.0' } }));
|
||
console.log('Installed from ' + (process.argv.find(arg => arg.startsWith('--registry=')) ?? 'the registry pnpm names'));
|
||
});
|
||
`)
|
||
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 }, locale: ZH_BROWSER_LOCALE })
|
||
const tripwire = watchConsole(page)
|
||
await page.goto(scaffold.authenticatedUrl)
|
||
await page.waitForSelector('[class*="frame"]')
|
||
if (await page.getByRole('dialog', { name: '设置' }).count() > 0) await page.keyboard.press('Escape')
|
||
await page.getByRole('navigation', { name: '全局面板' }).getByRole('button', { name: '插件', exact: true }).click()
|
||
const panel = page.locator('[data-plugin-panel]')
|
||
await panel.getByRole('button', { name: '添加插件', exact: true }).click()
|
||
const dialog = page.getByRole('dialog')
|
||
// Folded, the registry control names pnpm's own registry; unfolded, its options float from it and offer the configured
|
||
// mirror and a typed address.
|
||
const registryToggle = dialog.getByRole('button', { name: '安装源 默认安装源', exact: true })
|
||
await registryToggle.waitFor()
|
||
expect(await page.getByRole('radio').count()).toBe(0)
|
||
await registryToggle.click()
|
||
const options = page.getByRole('group', { name: '从哪个 npm 源下载插件', exact: true })
|
||
await options.getByRole('radio', { name: '中国大陆镜像源(registry.npmmirror.com)', exact: true }).click()
|
||
expect(await options.getByRole('radio').count()).toBe(3)
|
||
await dialog.getByRole('button', { name: '安装源 中国大陆镜像源', exact: true }).waitFor()
|
||
await dialog.getByRole('textbox', { name: '包名或地址' }).fill('mirrored-package')
|
||
const picker = (await captureStableAria(page, '[data-install-registry]', scaffold.workspaceCwd))
|
||
.split(process.execPath).join('{{node}}')
|
||
.split(scaffold.harnessHome).join('{{harnessHome}}')
|
||
await compareOrRefreshGolden(fileURLToPath(new URL('./expected/plugin-install-registry/picker.expected.md', import.meta.url)), picker, webSnapshotMode())
|
||
// The options float over the dialog rather than inside it: the card keeps its height with them open, and Escape folds them.
|
||
expect(await dialog.locator('[data-install-registry]').count()).toBe(0)
|
||
await page.keyboard.press('Escape')
|
||
await options.waitFor({ state: 'detached' })
|
||
await dialog.getByRole('button', { name: '安装源 中国大陆镜像源', exact: true }).waitFor()
|
||
await dialog.getByRole('button', { name: '安装', exact: true }).click()
|
||
// The check asked the mirror first, which answered; the run that lost the mirror sent the install on to
|
||
// the registry after it, which finished it. Each run shows behind the details with the registry it asked.
|
||
await dialog.getByRole('button', { name: '立即启用', exact: true }).waitFor({ timeout: 20_000 })
|
||
await dialog.getByText('版本 2.0.0', { exact: true }).waitFor()
|
||
await dialog.getByRole('button', { name: '查看安装详情', exact: true }).click()
|
||
await dialog.getByText('Installed from the registry pnpm names', { exact: true }).waitFor()
|
||
await dialog.getByText('第 1 次 · 中国大陆镜像源', { exact: true }).waitFor()
|
||
await dialog.getByText('第 2 次 · 默认安装源', { exact: true }).waitFor()
|
||
expect(JSON.parse(await readFile(manifestPath, 'utf8'))).toMatchObject({ dependencies: { 'mirrored-package': '2.0.0' } })
|
||
const installed = (await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd))
|
||
.split(process.execPath).join('{{node}}')
|
||
.split(scaffold.harnessHome).join('{{harnessHome}}')
|
||
await compareOrRefreshGolden(fileURLToPath(new URL('./expected/plugin-install-registry/installed.expected.md', import.meta.url)), installed, webSnapshotMode())
|
||
// The dialog opened again starts from the registry picked for the last install.
|
||
await dialog.getByRole('button', { name: '立即启用', exact: true }).click()
|
||
await panel.getByRole('button', { name: '添加插件', exact: true }).click()
|
||
await page.getByRole('dialog').getByRole('button', { name: '安装源 中国大陆镜像源', exact: true }).waitFor()
|
||
expect(tripwire.pageErrors).toEqual([])
|
||
} finally { await browser.close() }
|
||
} finally {
|
||
await scaffold?.close()
|
||
await rm(scratch, { recursive: true, force: true })
|
||
}
|
||
})
|