test: design-lab 加 E2e 防画面偏差——结构契约断言 + 五页视觉基线(reducedMotion 稳定截图)
This commit is contained in:
1 parent
869e38aef7
commit
15f66a8fa6
15 files changed
+296
-11
No files matched your search
@@ -0,0 +1,26 @@
|
|||||||
|
# design-lab · 设计系统可运行版
|
||||||
|
|
||||||
|
孟菲斯 × 拟物卡设计系统的 Vite MPA 实现。设计规格文档见 `design-refs/mockup/design-system/`(四册 PDF)。
|
||||||
|
|
||||||
|
## 结构
|
||||||
|
|
||||||
|
- 根目录每个 `*.html` 是一个入口,对应 `src/pages/<name>/main.tsx`(vite.config 自动扫描)
|
||||||
|
- `src/styles/tokens.css`:Tailwind v4 `@theme` 设计 token(撞色对 / 中性阶 / 章节色)
|
||||||
|
- `src/components/`:MemphisCard、RouteLine、Ticket、shapes、EntryGuide
|
||||||
|
- `src/lib/chapters.ts`:章节/关系/画像数据(改内容只动这里)
|
||||||
|
|
||||||
|
## 命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn dev # 本地开发
|
||||||
|
yarn build # 构建 MPA
|
||||||
|
yarn test:e2e # E2E:结构契约断言 + 视觉基线
|
||||||
|
yarn test:e2e:update # 有意的设计变更后,更新截图基线并提交
|
||||||
|
```
|
||||||
|
|
||||||
|
## E2E 防画面偏差(两层)
|
||||||
|
|
||||||
|
1. **结构契约**(`e2e/pages.spec.ts`):规格级断言——卡片 2px 描边/硬投影/纸叠三层、16:9 相框比例与内阴影、积木点实心数、站点 r6 半嵌(源骑黑影外缘/目标贴描边)、票券裁切式(overflow hidden + 头部无框 + 双打孔 + 撕线虚/实)。布局微调不误报,破坏规格必红。
|
||||||
|
2. **视觉基线**(`e2e/visual.spec.ts` + `e2e/visual.spec.ts-snapshots/`):五页整页截图像素比对,容差 1%。**基线需随代码提交**;有意变更用 `yarn test:e2e:update` 生成新基线后一并提交。
|
||||||
|
|
||||||
|
测试在 `prefers-reduced-motion` 下运行(卡片的 8000ms 翻转与动效关闭,截图稳定)。
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import { expect, test } from '@playwright/test'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设计系统结构契约断言——规格级防回归。
|
||||||
|
* 布局微调不误报,但破坏设计规格(描边/投影/比例/半嵌/裁切)必红。
|
||||||
|
*/
|
||||||
|
|
||||||
|
const PAGES = ['index', 'colors', 'symbols', 'components', 'shelf'] as const
|
||||||
|
|
||||||
|
test.describe('全局画布', () => {
|
||||||
|
for (const p of PAGES) {
|
||||||
|
test(`${p}.html 渲染且有内容`, async ({ page }) => {
|
||||||
|
await page.goto(`/${p}.html`)
|
||||||
|
await expect(page.locator('#root')).not.toBeEmpty()
|
||||||
|
await expect(page.locator('main')).toBeVisible()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
test('body 大底为纸色 #FFF9F0', async ({ page }) => {
|
||||||
|
await page.goto('/index.html')
|
||||||
|
const bg = await page.evaluate(() => getComputedStyle(document.body).backgroundColor)
|
||||||
|
expect(bg).toBe('rgb(255, 249, 240)')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('全站无色彩渐变', async ({ page }) => {
|
||||||
|
await page.goto('/shelf.html')
|
||||||
|
const gradients = await page.evaluate(() => {
|
||||||
|
const bad: string[] = []
|
||||||
|
document.querySelectorAll<HTMLElement>('*').forEach((el) => {
|
||||||
|
if (getComputedStyle(el).backgroundImage.includes('gradient')) bad.push(el.tagName)
|
||||||
|
})
|
||||||
|
return bad
|
||||||
|
})
|
||||||
|
expect(gradients, gradients.join(',')).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test.describe('CourseCard 规格契约', () => {
|
||||||
|
test.beforeEach(async ({ page }) => await page.goto('/components.html'))
|
||||||
|
|
||||||
|
test('2px ink 描边 + 硬投影 + 纸叠三层边', async ({ page }) => {
|
||||||
|
const card = page.locator('a[href="#ch1"]').first()
|
||||||
|
await expect(card).toBeVisible()
|
||||||
|
const s = await card.evaluate((el) => {
|
||||||
|
const cs = getComputedStyle(el)
|
||||||
|
return { bw: cs.borderTopWidth, bc: cs.borderTopColor, shadow: cs.boxShadow }
|
||||||
|
})
|
||||||
|
expect(s.bw).toBe('2px')
|
||||||
|
expect(s.bc).toBe('rgb(17, 17, 17)')
|
||||||
|
expect((s.shadow.match(/rgb|#/g) ?? []).length).toBeGreaterThanOrEqual(4) // 三层纸叠+硬投影(按色值计数)
|
||||||
|
expect(s.shadow).toContain('rgb(17, 17, 17)')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('16:9 相框:比例、3px 黑边、内阴影、章节撞色', async ({ page }) => {
|
||||||
|
const zone = page.locator('a[href="#ch1"] .aspect-video').first()
|
||||||
|
const s = await zone.evaluate((el) => {
|
||||||
|
const cs = getComputedStyle(el)
|
||||||
|
const r = el.getBoundingClientRect()
|
||||||
|
return { ratio: r.width / r.height, bw: cs.borderTopWidth, shadow: cs.boxShadow, bg: cs.backgroundColor }
|
||||||
|
})
|
||||||
|
expect(Math.abs(s.ratio - 16 / 9)).toBeLessThan(0.02)
|
||||||
|
expect(s.bw).toBe('3px')
|
||||||
|
expect(s.shadow).toContain('inset')
|
||||||
|
expect(s.bg).toBe('rgb(255, 107, 107)') // ch1 coral 只进相框
|
||||||
|
})
|
||||||
|
|
||||||
|
test('卡头三件套:▲ + 标题 + 积木点(实心=已学)', async ({ page }) => {
|
||||||
|
const card = page.locator('a[href="#ch2"]').first()
|
||||||
|
const dots = card.locator('span[aria-label^="积木"] i')
|
||||||
|
expect(await dots.count()).toBe(4)
|
||||||
|
const filled = await dots.evaluateAll((ns) => ns.filter((n) => getComputedStyle(n).backgroundColor === 'rgb(17, 17, 17)').length)
|
||||||
|
expect(filled).toBe(2) // ch2 已学 2/4
|
||||||
|
expect(await card.locator('svg').first()).toBeVisible() // ▲
|
||||||
|
})
|
||||||
|
|
||||||
|
test('入口徽章 ①②(推荐起点,非强制)', async ({ page }) => {
|
||||||
|
await expect(page.locator('span[aria-label="推荐入口 1"]')).toBeVisible()
|
||||||
|
await expect(page.locator('span[aria-label="推荐入口 2"]')).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test.describe('RouteLine 规格契约', () => {
|
||||||
|
test.beforeEach(async ({ page }) => await page.goto('/shelf.html'))
|
||||||
|
|
||||||
|
test('线形:实线先后 2.5px 无 dash;点线同族 0.1/6 圆帽', async ({ page }) => {
|
||||||
|
const specs = await page.locator('svg path').evaluateAll((ns) =>
|
||||||
|
ns.map((n) => ({ w: n.getAttribute('stroke-width'), dash: n.getAttribute('stroke-dasharray'), cap: n.getAttribute('stroke-linecap') })),
|
||||||
|
)
|
||||||
|
expect(specs.some((s) => s.w === '2.5' && !s.dash)).toBeTruthy()
|
||||||
|
expect(specs.some((s) => s.dash === '0.1 6' && s.cap === 'round')).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('虚线相关 4:2(symbols 页线形样张)', async ({ page }) => {
|
||||||
|
await page.goto('/symbols.html')
|
||||||
|
const specs = await page.locator('svg path').evaluateAll((ns) => ns.map((n) => n.getAttribute('stroke-dasharray')))
|
||||||
|
expect(specs).toContain('4 2')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('站点 r6 半嵌:源骑卡右黑影外缘、目标贴左描边', async ({ page }) => {
|
||||||
|
const c1 = await page.locator('a[href="#ch1"]').first().boundingBox()
|
||||||
|
const c2 = await page.locator('a[href="#ch2"]').first().boundingBox()
|
||||||
|
const wrap = await page.locator('main div.relative').first().boundingBox()
|
||||||
|
expect(c1 && c2 && wrap).toBeTruthy()
|
||||||
|
|
||||||
|
const stations = await page.locator('svg circle').evaluateAll((ns) =>
|
||||||
|
ns.map((n) => ({ cx: +(n.getAttribute('cx') ?? 0), r: +(n.getAttribute('r') ?? 0) })).filter((s) => s.r === 6), // 排除散落 ○ 预期环
|
||||||
|
)
|
||||||
|
expect(stations.length).toBeGreaterThanOrEqual(4)
|
||||||
|
|
||||||
|
const c1ShadowEdge = c1!.x - wrap!.x + c1!.width + 7 // 描边外 + 7px 黑影
|
||||||
|
const c2Border = c2!.x - wrap!.x
|
||||||
|
expect(stations.some((s) => Math.abs(s.cx - c1ShadowEdge) <= 3), `源站点应骑黑影外缘 x≈${c1ShadowEdge}`).toBeTruthy()
|
||||||
|
expect(stations.some((s) => Math.abs(s.cx - c2Border) <= 3), `目标站点应贴左描边 x≈${c2Border}`).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('实线端点落在水平端口(M x y H 起、H x 止)', async ({ page }) => {
|
||||||
|
const d = await page.locator('svg path[stroke-width="2.5"]').first().getAttribute('d')
|
||||||
|
expect(d).toMatch(/^M [\d.]+ [\d.]+ H/)
|
||||||
|
expect(d).toMatch(/H [\d.]+$/)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test.describe('票券规格契约', () => {
|
||||||
|
test.beforeEach(async ({ page }) => await page.goto('/symbols.html'))
|
||||||
|
|
||||||
|
test('裁切式:overflow hidden + 单一 2px 描边 + 头部色块无独立边框', async ({ page }) => {
|
||||||
|
const done = page.locator('.ticket-pk').first()
|
||||||
|
await expect(done).toBeVisible()
|
||||||
|
const s = await done.evaluate((el) => {
|
||||||
|
const cs = getComputedStyle(el)
|
||||||
|
const head = el.firstElementChild as HTMLElement
|
||||||
|
const hcs = getComputedStyle(head)
|
||||||
|
return { ov: cs.overflow, bw: cs.borderTopWidth, headBorder: hcs.borderTopWidth, headBg: hcs.backgroundColor }
|
||||||
|
})
|
||||||
|
expect(s.ov).toBe('hidden')
|
||||||
|
expect(s.bw).toBe('2px')
|
||||||
|
expect(s.headBorder).toBe('0px') // 头部色块永不自带描边
|
||||||
|
expect(s.headBg).not.toBe('rgba(0, 0, 0, 0)')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('打孔伪元素在位(完成票 ×2 孔)', async ({ page }) => {
|
||||||
|
const holes = await page.locator('.ticket-pk').first().evaluate((el) => {
|
||||||
|
const b = getComputedStyle(el, '::before')
|
||||||
|
const a = getComputedStyle(el, '::after')
|
||||||
|
return [b.content, a.content].filter((c) => c !== 'none').length
|
||||||
|
})
|
||||||
|
expect(holes).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('撕线:完成=虚线、未开始=实线', async ({ page }) => {
|
||||||
|
const styles = await page.evaluate(() =>
|
||||||
|
[...document.querySelectorAll('div')]
|
||||||
|
.filter((el) => String(el.className).includes('w-[216px]'))
|
||||||
|
.map((el) => getComputedStyle(el.lastElementChild as HTMLElement).borderLeftStyle),
|
||||||
|
)
|
||||||
|
expect(styles).toContain('dashed')
|
||||||
|
expect(styles).toContain('solid')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { expect, test } from '@playwright/test'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 像素级视觉回归基线——防止后续改动出现画面偏差。
|
||||||
|
* 有意的设计变更:`yarn test:e2e -- --update-snapshots` 后提交新基线。
|
||||||
|
*/
|
||||||
|
const CASES: [name: string, path: string][] = [
|
||||||
|
['index', '/index.html'],
|
||||||
|
['colors', '/colors.html'],
|
||||||
|
['symbols', '/symbols.html'],
|
||||||
|
['components', '/components.html'],
|
||||||
|
['shelf', '/shelf.html'],
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const [name, path] of CASES) {
|
||||||
|
test(`视觉基线 · ${name}`, async ({ page }) => {
|
||||||
|
await page.goto(path)
|
||||||
|
await page.evaluate(() => document.fonts.ready)
|
||||||
|
await page.waitForTimeout(300)
|
||||||
|
await expect(page).toHaveScreenshot(`${name}.png`, {
|
||||||
|
fullPage: true,
|
||||||
|
maxDiffPixelRatio: 0.01, // 允许 1% 以内的字体渲染差异
|
||||||
|
animations: 'disabled',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 172 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 179 KiB |
@@ -6,13 +6,16 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:update": "playwright test --update-snapshots"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1"
|
"react-dom": "^18.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "1.63.0",
|
||||||
"@tailwindcss/vite": "^4.1.0",
|
"@tailwindcss/vite": "^4.1.0",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设计系统 E2E:防止后续改动出现画面偏差。
|
||||||
|
* - pages.spec:结构契约断言(规格级:描边/投影/比例/半嵌坐标)
|
||||||
|
* - visual.spec:整页截图基线(像素级回归)
|
||||||
|
* 截图基线在 e2e/__screenshots__/,需随代码一起提交;
|
||||||
|
* 有意的设计变更请运行 `yarn test:e2e -- --update-snapshots` 后提交新基线。
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: 'e2e',
|
||||||
|
timeout: 30_000,
|
||||||
|
fullyParallel: true,
|
||||||
|
use: {
|
||||||
|
baseURL: 'http://localhost:4490',
|
||||||
|
// 卡片尊重 prefers-reduced-motion:翻转关闭、动画停用,截图才稳定
|
||||||
|
contextOptions: { reducedMotion: 'reduce', viewport: { width: 1280, height: 900 } },
|
||||||
|
},
|
||||||
|
webServer: {
|
||||||
|
command: 'yarn preview --port 4490 --strictPort',
|
||||||
|
url: 'http://localhost:4490/index.html',
|
||||||
|
reuseExistingServer: true,
|
||||||
|
},
|
||||||
|
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
|
||||||
|
})
|
||||||
@@ -6,7 +6,6 @@ export function Ticket({ chapter, done }: { chapter: Chapter; done: boolean }) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`relative flex w-[216px] items-stretch overflow-hidden rounded-xl border-2 border-ink bg-paper shadow-[5px_5px_0_var(--color-ink)] ${done ? 'ticket-pk' : ''}`}
|
className={`relative flex w-[216px] items-stretch overflow-hidden rounded-xl border-2 border-ink bg-paper shadow-[5px_5px_0_var(--color-ink)] ${done ? 'ticket-pk' : ''}`}
|
||||||
style={{ ['--tk-color' as string]: chapter.color }}
|
|
||||||
>
|
>
|
||||||
{/* 头部媒体块:章节色纯填充,无独立边框(由票体裁切) */}
|
{/* 头部媒体块:章节色纯填充,无独立边框(由票体裁切) */}
|
||||||
<div className="flex w-14 shrink-0 items-center justify-center text-[22px]" style={{ background: chapter.color }}>{chapter.emoji}</div>
|
<div className="flex w-14 shrink-0 items-center justify-center text-[22px]" style={{ background: chapter.color }}>{chapter.emoji}</div>
|
||||||
@@ -21,14 +20,6 @@ export function Ticket({ chapter, done }: { chapter: Chapter; done: boolean }) {
|
|||||||
>
|
>
|
||||||
№{String(chapter.id).padStart(2, '0')}
|
№{String(chapter.id).padStart(2, '0')}
|
||||||
</div>
|
</div>
|
||||||
<style>{`
|
|
||||||
.ticket-pk::before, .ticket-pk::after {
|
|
||||||
content: ''; position: absolute; left: 160px; width: 14px; height: 14px;
|
|
||||||
border-radius: 50%; background: var(--color-paper); border: 2px solid var(--color-ink);
|
|
||||||
}
|
|
||||||
.ticket-pk::before { top: -10px; }
|
|
||||||
.ticket-pk::after { bottom: -10px; }
|
|
||||||
`}</style>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Bar, Cross, MissMark, Ring, Tremble, Tri, Wave } from '../../components
|
|||||||
const nodes = {
|
const nodes = {
|
||||||
1: { id: 1, borderLeft: 40, right: 299, y: 180 },
|
1: { id: 1, borderLeft: 40, right: 299, y: 180 },
|
||||||
2: { id: 2, borderLeft: 378, right: 639, y: 108 },
|
2: { id: 2, borderLeft: 378, right: 639, y: 108 },
|
||||||
4: { id: 4, borderLeft: 698, right: 848, y: 108 },
|
5: { id: 5, borderLeft: 698, right: 848, y: 108 },
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
const SCATTER = [
|
const SCATTER = [
|
||||||
|
|||||||
@@ -31,3 +31,22 @@
|
|||||||
.sem-shadow {
|
.sem-shadow {
|
||||||
filter: drop-shadow(0 1px 0 rgba(255, 255, 255, 0.9));
|
filter: drop-shadow(0 1px 0 rgba(255, 255, 255, 0.9));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 票券打孔:⌀14 圆跨边,被 overflow-hidden 裁成半圆缺口(完成票专用) */
|
||||||
|
.ticket-pk::before,
|
||||||
|
.ticket-pk::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 160px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-paper);
|
||||||
|
border: 2px solid var(--color-ink);
|
||||||
|
}
|
||||||
|
.ticket-pk::before {
|
||||||
|
top: -10px;
|
||||||
|
}
|
||||||
|
.ticket-pk::after {
|
||||||
|
bottom: -10px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"status": "passed",
|
||||||
|
"failedTests": []
|
||||||
|
}
|
||||||
@@ -3172,6 +3172,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@playwright/test@npm:1.63.0":
|
||||||
|
version: 1.63.0
|
||||||
|
resolution: "@playwright/test@npm:1.63.0"
|
||||||
|
dependencies:
|
||||||
|
playwright: "npm:1.63.0"
|
||||||
|
bin:
|
||||||
|
playwright: cli.js
|
||||||
|
checksum: 10/b5e561b8f11488b8062b98c5cf975740f48ae7e9b455fb7152e0fb90d8e13f20c3c3ae6a15833655364246b99f5a46f6601daa5a9eef01de5004c8be7e2b3af3
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@pnpm/config.env-replace@npm:^1.1.0":
|
"@pnpm/config.env-replace@npm:^1.1.0":
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
resolution: "@pnpm/config.env-replace@npm:1.1.0"
|
resolution: "@pnpm/config.env-replace@npm:1.1.0"
|
||||||
@@ -3447,6 +3458,7 @@ __metadata:
|
|||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@stack/design-lab@workspace:apps/design-lab"
|
resolution: "@stack/design-lab@workspace:apps/design-lab"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
"@playwright/test": "npm:1.63.0"
|
||||||
"@tailwindcss/vite": "npm:^4.1.0"
|
"@tailwindcss/vite": "npm:^4.1.0"
|
||||||
"@types/react": "npm:^18.3.12"
|
"@types/react": "npm:^18.3.12"
|
||||||
"@types/react-dom": "npm:^18.3.1"
|
"@types/react-dom": "npm:^18.3.1"
|
||||||
@@ -9905,6 +9917,26 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"playwright-core@npm:1.63.0":
|
||||||
|
version: 1.63.0
|
||||||
|
resolution: "playwright-core@npm:1.63.0"
|
||||||
|
bin:
|
||||||
|
playwright-core: cli.js
|
||||||
|
checksum: 10/d0f7c70786f5e08aa7553a77ba28c07427086e323ae8c4126a647debd8181966dc11d6f8fe1462f2a776ad60c95451b08f01fbee17b11e69a5bd9a2070fe75a6
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"playwright@npm:1.63.0":
|
||||||
|
version: 1.63.0
|
||||||
|
resolution: "playwright@npm:1.63.0"
|
||||||
|
dependencies:
|
||||||
|
playwright-core: "npm:1.63.0"
|
||||||
|
bin:
|
||||||
|
playwright: cli.js
|
||||||
|
checksum: 10/ec87c22888148603a8955bc2a092f416455af931fe53925b9143ce333b83e42a6d8c159c00a4f2d232b27100cf3eadd93cec8a6e5c4e12c1c00663d2034dd515
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"postcss-attribute-case-insensitive@npm:^7.0.1":
|
"postcss-attribute-case-insensitive@npm:^7.0.1":
|
||||||
version: 7.0.1
|
version: 7.0.1
|
||||||
resolution: "postcss-attribute-case-insensitive@npm:7.0.1"
|
resolution: "postcss-attribute-case-insensitive@npm:7.0.1"
|
||||||
|
|||||||
Reference in new issue
Block a user