slidev-theme-practicum 0.1.4 → 0.3.0
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 +238 -18
- package/components/Slide.vue +27 -4
- package/components/Slot.vue +2 -1
- package/components/StepsGrid.vue +112 -0
- package/composables/deck-decors.ts +74 -0
- package/composables/decor-sources.ts +43 -0
- package/composables/layout-authoring.ts +34 -9
- package/composables/layout-recipes.ts +83 -2
- package/composables/layout-shorthands.ts +233 -2
- package/composables/local-layout-variant-files.ts +106 -0
- package/composables/local-layout-variants.ts +73 -0
- package/composables/slide-layout.ts +3 -0
- package/composables/use-theme-config.ts +17 -10
- package/composables/validate-deck-layouts.cjs +77 -6
- package/env.d.ts +18 -0
- package/example.md +135 -5
- package/package.json +6 -3
- package/scripts/browser-smoke.mjs +168 -5
- package/scripts/check-local-layout-variant-build.mjs +189 -0
- package/scripts/check-package.mjs +18 -2
- package/scripts/requirements-illustrations.txt +1 -0
- package/scripts/trace-line-art.py +422 -0
- package/scripts/validate-deck.cjs +1 -1
- package/setup/vite-plugins.ts +109 -2
- package/skills/slidev-practicum/SKILL.md +18 -4
- package/skills/slidev-practicum/references/contour-illustrations.md +114 -0
- package/skills/slidev-practicum/references/deck-project-structure.md +136 -0
- package/skills/slidev-practicum/references/illustration-examples/balance-scales.png +0 -0
- package/skills/slidev-practicum/references/illustration-examples/balance-scales.svg +88 -0
- package/skills/slidev-practicum/references/illustration-examples/chainsaw.png +0 -0
- package/skills/slidev-practicum/references/illustration-examples/chainsaw.svg +4 -0
- package/skills/slidev-practicum/references/illustration-examples/graduation-cap.png +0 -0
- package/skills/slidev-practicum/references/illustration-examples/graduation-cap.svg +23 -0
- package/skills/slidev-practicum/references/illustration-examples/woodcutter-axe.png +0 -0
- package/skills/slidev-practicum/references/illustration-examples/woodcutter-axe.svg +4 -0
- package/skills/slidev-practicum/references/photographic-illustrations.md +100 -0
|
@@ -10,8 +10,28 @@ import {
|
|
|
10
10
|
|
|
11
11
|
const PROJECT_ROOT = resolve(import.meta.dirname, '..')
|
|
12
12
|
const DIST_DIR = resolve(PROJECT_ROOT, 'dist')
|
|
13
|
-
const
|
|
13
|
+
const EXPECTED_DOCUMENT_TITLE = 'Яндекс Практикум — справочная галерея - Slidev'
|
|
14
|
+
const REPRESENTATIVE_SLIDES = [1, 9, 15, 20, 38, 46, 47]
|
|
14
15
|
const VISUAL_ONLY_SLIDES = [17, 18]
|
|
16
|
+
const P0_REGRESSION_SLIDES = [23, 24, 25, 26, 27, 28]
|
|
17
|
+
const P0_FRONTMATTER_TEXT = {
|
|
18
|
+
23: {
|
|
19
|
+
selector: '.Slide_variant_comparison-before-after',
|
|
20
|
+
values: ['«Я просто запомнил формулу»', '«Я выбрал формулу по условию»'],
|
|
21
|
+
},
|
|
22
|
+
24: {
|
|
23
|
+
selector: '.Slide_variant_comparison-stable-variable',
|
|
24
|
+
values: ['Научиться проверять гипотезу', 'Данные и уровень подсказок'],
|
|
25
|
+
},
|
|
26
|
+
25: {
|
|
27
|
+
selector: '.Slide-StepsGrid_linear',
|
|
28
|
+
values: ['Назвать цель', 'Дать контекст', 'Показать пример', 'Оставить практику', 'Проверить критерий', 'Закрепить вывод'],
|
|
29
|
+
},
|
|
30
|
+
26: {
|
|
31
|
+
selector: '.Slide-StepsGrid_staggered',
|
|
32
|
+
values: ['Заметить сбой', 'Повторить шаг', 'Найти правило', 'Изменить подход', 'Записать вывод'],
|
|
33
|
+
},
|
|
34
|
+
}
|
|
15
35
|
const LIFECYCLE_EVENT = 'practicum:slide-lifecycle'
|
|
16
36
|
const LIFECYCLE_STATE_KEY = '__practicumBrowserLifecycle'
|
|
17
37
|
const THEME_MEDIA = [
|
|
@@ -39,6 +59,10 @@ function assertNoDiagnostics(label, diagnostics) {
|
|
|
39
59
|
)
|
|
40
60
|
}
|
|
41
61
|
|
|
62
|
+
function normalizeVisibleText(value) {
|
|
63
|
+
return value.replace(/\s+/g, ' ').trim()
|
|
64
|
+
}
|
|
65
|
+
|
|
42
66
|
async function settleSlide(page) {
|
|
43
67
|
await page.evaluate(async () => {
|
|
44
68
|
await Promise.race([
|
|
@@ -168,6 +192,122 @@ async function assertClosingLayout(slide, slideNumber, expectedTextCount) {
|
|
|
168
192
|
)
|
|
169
193
|
}
|
|
170
194
|
|
|
195
|
+
async function assertStaggeredStepsLayout(slide, slideNumber) {
|
|
196
|
+
const grid = slide.locator('.Slide-StepsGrid_staggered')
|
|
197
|
+
const cards = grid.locator(':scope > .Slide-Step')
|
|
198
|
+
const cardCount = await cards.count()
|
|
199
|
+
|
|
200
|
+
assert.equal(cardCount, 5, `слайд ${slideNumber}: staggered должен содержать ровно 5 карточек`)
|
|
201
|
+
|
|
202
|
+
const [gridBox, cardBoxes, placements] = await Promise.all([
|
|
203
|
+
grid.boundingBox(),
|
|
204
|
+
Promise.all(Array.from({ length: cardCount }, (_, index) => cards.nth(index).boundingBox())),
|
|
205
|
+
cards.evaluateAll(elements => elements.map((element) => {
|
|
206
|
+
const style = getComputedStyle(element)
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
columnEnd: style.gridColumnEnd,
|
|
210
|
+
columnStart: style.gridColumnStart,
|
|
211
|
+
rowEnd: style.gridRowEnd,
|
|
212
|
+
rowStart: style.gridRowStart,
|
|
213
|
+
}
|
|
214
|
+
})),
|
|
215
|
+
])
|
|
216
|
+
|
|
217
|
+
assert.ok(gridBox, `слайд ${slideNumber}: не найдена геометрия staggered grid`)
|
|
218
|
+
assert.ok(cardBoxes.every(Boolean), `слайд ${slideNumber}: не найдена геометрия всех staggered-карточек`)
|
|
219
|
+
assert.deepEqual(placements, [
|
|
220
|
+
{ columnEnd: '3', columnStart: '1', rowEnd: '2', rowStart: '1' },
|
|
221
|
+
{ columnEnd: '5', columnStart: '3', rowEnd: '2', rowStart: '1' },
|
|
222
|
+
{ columnEnd: '7', columnStart: '5', rowEnd: '2', rowStart: '1' },
|
|
223
|
+
{ columnEnd: '4', columnStart: '2', rowEnd: '3', rowStart: '2' },
|
|
224
|
+
{ columnEnd: '6', columnStart: '4', rowEnd: '3', rowStart: '2' },
|
|
225
|
+
], `слайд ${slideNumber}: staggered должен использовать центрированную геометрию 3+2`)
|
|
226
|
+
|
|
227
|
+
const boxes = cardBoxes
|
|
228
|
+
const topY = boxes[0].y
|
|
229
|
+
const bottomY = boxes[3].y
|
|
230
|
+
|
|
231
|
+
assert.ok(
|
|
232
|
+
boxes.slice(0, 3).every(box => Math.abs(box.y - topY) <= 1),
|
|
233
|
+
`слайд ${slideNumber}: первые три staggered-карточки должны быть в верхнем ряду`,
|
|
234
|
+
)
|
|
235
|
+
assert.ok(
|
|
236
|
+
boxes.slice(3).every(box => Math.abs(box.y - bottomY) <= 1) && bottomY > topY,
|
|
237
|
+
`слайд ${slideNumber}: последние две staggered-карточки должны быть в нижнем ряду`,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
const lowerCenter = (boxes[3].x + boxes[4].x + boxes[4].width) / 2
|
|
241
|
+
const gridCenter = gridBox.x + gridBox.width / 2
|
|
242
|
+
|
|
243
|
+
assert.ok(
|
|
244
|
+
Math.abs(lowerCenter - gridCenter) <= 1,
|
|
245
|
+
`слайд ${slideNumber}: нижняя пара staggered не центрирована`,
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function assertNumberedQuartet(slide, slideNumber) {
|
|
250
|
+
const markers = slide.locator('.Text_size_4')
|
|
251
|
+
assert.deepEqual(
|
|
252
|
+
(await markers.allTextContents()).map(text => text.trim()),
|
|
253
|
+
['1', 'Цель', '2', 'Контекст', '3', 'Критерий', '4', 'Обратная связь'],
|
|
254
|
+
`слайд ${slideNumber}: номер и название каждого факта должны быть plain Text размера 4`,
|
|
255
|
+
)
|
|
256
|
+
assert.equal(
|
|
257
|
+
await slide.locator('.Slide-FactNumber').count(),
|
|
258
|
+
0,
|
|
259
|
+
`слайд ${slideNumber}: номера не должны оформляться кружками`,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
const factTitles = slide.locator('.Text_size_4.Text_align_end')
|
|
263
|
+
assert.deepEqual(
|
|
264
|
+
(await factTitles.allTextContents()).map(text => text.trim()),
|
|
265
|
+
['Цель', 'Контекст', 'Критерий', 'Обратная связь'],
|
|
266
|
+
`слайд ${slideNumber}: названия фактов должны быть прижаты к нижнему краю карточки`,
|
|
267
|
+
)
|
|
268
|
+
assert.ok(
|
|
269
|
+
(await factTitles.evaluateAll(elements => elements.every((element) => {
|
|
270
|
+
const marginTop = Number.parseFloat(getComputedStyle(element).marginTop)
|
|
271
|
+
return Number.isFinite(marginTop) && marginTop > 0
|
|
272
|
+
}))),
|
|
273
|
+
`слайд ${slideNumber}: названия фактов должны быть отодвинуты вниз от номера`,
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function assertP0FrontmatterText(slide, slideNumber) {
|
|
278
|
+
const contract = P0_FRONTMATTER_TEXT[slideNumber]
|
|
279
|
+
if (!contract)
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
assert.equal(
|
|
283
|
+
await slide.locator(contract.selector).count(),
|
|
284
|
+
1,
|
|
285
|
+
`слайд ${slideNumber}: не найден ожидаемый variant/arrangement ${contract.selector}`,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
const visibleText = await slide.locator('.Text').evaluateAll(elements => elements
|
|
289
|
+
.filter((element) => {
|
|
290
|
+
const rect = element.getBoundingClientRect()
|
|
291
|
+
const style = getComputedStyle(element)
|
|
292
|
+
|
|
293
|
+
return rect.width > 0
|
|
294
|
+
&& rect.height > 0
|
|
295
|
+
&& style.display !== 'none'
|
|
296
|
+
&& style.visibility !== 'hidden'
|
|
297
|
+
&& style.opacity !== '0'
|
|
298
|
+
})
|
|
299
|
+
.map(element => element.textContent ?? ''))
|
|
300
|
+
const normalizedVisibleText = visibleText.map(normalizeVisibleText)
|
|
301
|
+
|
|
302
|
+
for (const expected of contract.values) {
|
|
303
|
+
assert.ok(
|
|
304
|
+
normalizedVisibleText.includes(normalizeVisibleText(expected)),
|
|
305
|
+
`слайд ${slideNumber}: вложенный текст front matter «${expected}» не виден; `
|
|
306
|
+
+ `видимые Text=${JSON.stringify(normalizedVisibleText)}`,
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
171
311
|
async function inspectSlide(
|
|
172
312
|
page,
|
|
173
313
|
origin,
|
|
@@ -222,12 +362,17 @@ async function inspectSlide(
|
|
|
222
362
|
result.heightOverflow <= 1,
|
|
223
363
|
`слайд ${slideNumber}: вертикальное переполнение ${result.heightOverflow}px`,
|
|
224
364
|
)
|
|
225
|
-
if (slideNumber ===
|
|
365
|
+
if (slideNumber === 38)
|
|
226
366
|
await assertStrongEmphasis(slide, slideNumber)
|
|
227
|
-
if (slideNumber ===
|
|
367
|
+
if (slideNumber === 46)
|
|
228
368
|
await assertClosingLayout(slide, slideNumber, 1)
|
|
229
|
-
if (slideNumber ===
|
|
369
|
+
if (slideNumber === 47)
|
|
230
370
|
await assertClosingLayout(slide, slideNumber, 2)
|
|
371
|
+
if (slideNumber === 26)
|
|
372
|
+
await assertStaggeredStepsLayout(slide, slideNumber)
|
|
373
|
+
if (slideNumber === 28)
|
|
374
|
+
await assertNumberedQuartet(slide, slideNumber)
|
|
375
|
+
await assertP0FrontmatterText(slide, slideNumber)
|
|
231
376
|
assertLifecycleTransition(
|
|
232
377
|
beforeLifecycle,
|
|
233
378
|
afterLifecycle,
|
|
@@ -387,6 +532,13 @@ export async function runBrowserSmoke() {
|
|
|
387
532
|
REPRESENTATIVE_SLIDES[index - 1],
|
|
388
533
|
() => inFlightTracker.waitForIdle(),
|
|
389
534
|
)
|
|
535
|
+
if (index === 0) {
|
|
536
|
+
assert.equal(
|
|
537
|
+
await page.title(),
|
|
538
|
+
EXPECTED_DOCUMENT_TITLE,
|
|
539
|
+
'первый headmatter title должен задавать служебный HTML title колоды',
|
|
540
|
+
)
|
|
541
|
+
}
|
|
390
542
|
}
|
|
391
543
|
|
|
392
544
|
for (const slideNumber of VISUAL_ONLY_SLIDES) {
|
|
@@ -400,6 +552,17 @@ export async function runBrowserSmoke() {
|
|
|
400
552
|
)
|
|
401
553
|
}
|
|
402
554
|
|
|
555
|
+
for (const slideNumber of P0_REGRESSION_SLIDES) {
|
|
556
|
+
activeSlide = `P0-регрессия: слайд ${slideNumber}`
|
|
557
|
+
await inspectSlide(
|
|
558
|
+
page,
|
|
559
|
+
server.origin,
|
|
560
|
+
slideNumber,
|
|
561
|
+
undefined,
|
|
562
|
+
() => inFlightTracker.waitForIdle(),
|
|
563
|
+
)
|
|
564
|
+
}
|
|
565
|
+
|
|
403
566
|
for (const asset of THEME_MEDIA) {
|
|
404
567
|
activeSlide = `медиа: ${asset.label}`
|
|
405
568
|
await loadThemeImage(page, server.origin, asset)
|
|
@@ -413,7 +576,7 @@ export async function runBrowserSmoke() {
|
|
|
413
576
|
assertNoDiagnostics('неуспешные запросы того же источника', failedRequests)
|
|
414
577
|
|
|
415
578
|
console.log(
|
|
416
|
-
`Браузерная проверка: слайды=${REPRESENTATIVE_SLIDES.length + VISUAL_ONLY_SLIDES.length}, `
|
|
579
|
+
`Браузерная проверка: слайды=${REPRESENTATIVE_SLIDES.length + VISUAL_ONLY_SLIDES.length + P0_REGRESSION_SLIDES.length}, `
|
|
417
580
|
+ `ошибкиСтраницы=${pageErrors.length}, ошибкиКонсоли=${consoleErrors.length}, `
|
|
418
581
|
+ `ошибочныеОтветы=${failedResponses.length}, активныеЗапросы=${inFlightTracker.size}, `
|
|
419
582
|
+ `переполнение=0, медиа=${THEME_MEDIA.length}, переходыЖЦ=${REPRESENTATIVE_SLIDES.length - 1}`,
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { spawnSync } from 'node:child_process'
|
|
3
|
+
import {
|
|
4
|
+
cpSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
} from 'node:fs'
|
|
11
|
+
import { join, resolve } from 'node:path'
|
|
12
|
+
import process from 'node:process'
|
|
13
|
+
import { chromium } from 'playwright-chromium'
|
|
14
|
+
import { createStaticDistServer } from './browser-smoke-runtime.mjs'
|
|
15
|
+
|
|
16
|
+
const PROJECT_ROOT = resolve(import.meta.dirname, '..')
|
|
17
|
+
const FIXTURE_ROOT = join(PROJECT_ROOT, 'tests/fixtures/local-layout-variant-deck')
|
|
18
|
+
const SLIDEV_CLI = join(PROJECT_ROOT, 'node_modules/@slidev/cli/bin/slidev.mjs')
|
|
19
|
+
const consumerParent = join(PROJECT_ROOT, '.slidev')
|
|
20
|
+
mkdirSync(consumerParent, { recursive: true })
|
|
21
|
+
const consumerRoot = mkdtempSync(join(consumerParent, 'layout-variant-consumer-'))
|
|
22
|
+
const deckRoot = join(consumerRoot, 'deck')
|
|
23
|
+
const outputRoot = join(deckRoot, 'dist')
|
|
24
|
+
|
|
25
|
+
const FRONTMATTER_SLIDES = [
|
|
26
|
+
{
|
|
27
|
+
number: 2,
|
|
28
|
+
selector: '.Slide-StepsGrid_linear',
|
|
29
|
+
values: ['Найти нужную версию', 'Восстановить контекст', 'Сгенерировать черновик', 'Проверить', 'Исправить', 'Перенести результат'],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
number: 3,
|
|
33
|
+
selector: '.Slide-StepsGrid_staggered',
|
|
34
|
+
values: ['Заметить сбой', 'Повторить шаг', 'Найти правило', 'Изменить подход', 'Записать вывод'],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
number: 4,
|
|
38
|
+
selector: '.Slide_variant_comparison-before-after',
|
|
39
|
+
values: ['Решение — за человеком', 'Стадия работает сама'],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
number: 5,
|
|
43
|
+
selector: '.Slide_variant_comparison-stable-variable',
|
|
44
|
+
values: ['Научиться проверять гипотезу', 'Данные и уровень подсказок'],
|
|
45
|
+
},
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
function normalizeVisibleText(value) {
|
|
49
|
+
return value.replace(/\s+/g, ' ').trim()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readArtifact(directory) {
|
|
53
|
+
return readdirSync(directory, { withFileTypes: true })
|
|
54
|
+
.flatMap((entry) => {
|
|
55
|
+
const path = join(directory, entry.name)
|
|
56
|
+
return entry.isDirectory() ? readArtifact(path) : [readFileSync(path)]
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function inspectRenderedVariant() {
|
|
61
|
+
const diagnostics = []
|
|
62
|
+
const server = await createStaticDistServer(outputRoot)
|
|
63
|
+
const browser = await chromium.launch({ headless: true })
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const context = await browser.newContext({ serviceWorkers: 'block' })
|
|
67
|
+
await context.grantPermissions(['screen-wake-lock'], { origin: server.origin })
|
|
68
|
+
const page = await context.newPage()
|
|
69
|
+
page.on('console', (message) => {
|
|
70
|
+
if (message.type() === 'error')
|
|
71
|
+
diagnostics.push(message.text())
|
|
72
|
+
})
|
|
73
|
+
page.on('pageerror', error => diagnostics.push(error.message))
|
|
74
|
+
|
|
75
|
+
await page.goto(`${server.origin}/1`, { waitUntil: 'networkidle' })
|
|
76
|
+
|
|
77
|
+
const firstSlide = page.locator('.slidev-page[data-slidev-no="1"]')
|
|
78
|
+
const badge = firstSlide.locator('.LocalLayoutVariantBadge')
|
|
79
|
+
await firstSlide.waitFor({ state: 'visible' })
|
|
80
|
+
await badge.waitFor({ state: 'visible' })
|
|
81
|
+
|
|
82
|
+
assert.equal((await badge.textContent())?.trim(), 'Практика')
|
|
83
|
+
assert.equal((await firstSlide.locator('h1').textContent())?.trim(), 'Что запомнить')
|
|
84
|
+
|
|
85
|
+
for (const contract of FRONTMATTER_SLIDES) {
|
|
86
|
+
await page.goto(`${server.origin}/${contract.number}`, { waitUntil: 'networkidle' })
|
|
87
|
+
const slide = page.locator(`.slidev-page[data-slidev-no="${contract.number}"]`)
|
|
88
|
+
await slide.waitFor({ state: 'visible' })
|
|
89
|
+
|
|
90
|
+
assert.equal(
|
|
91
|
+
await slide.locator(contract.selector).count(),
|
|
92
|
+
1,
|
|
93
|
+
`слайд ${contract.number}: не найден ${contract.selector}`,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
const rendered = await slide.locator('.Text').evaluateAll(elements => elements
|
|
97
|
+
.filter((element) => {
|
|
98
|
+
const rect = element.getBoundingClientRect()
|
|
99
|
+
const style = getComputedStyle(element)
|
|
100
|
+
|
|
101
|
+
return rect.width > 0
|
|
102
|
+
&& rect.height > 0
|
|
103
|
+
&& style.display !== 'none'
|
|
104
|
+
&& style.visibility !== 'hidden'
|
|
105
|
+
&& style.opacity !== '0'
|
|
106
|
+
})
|
|
107
|
+
.map(element => element.textContent ?? ''))
|
|
108
|
+
const normalized = rendered.map(normalizeVisibleText)
|
|
109
|
+
|
|
110
|
+
for (const expected of contract.values) {
|
|
111
|
+
assert.ok(
|
|
112
|
+
normalized.includes(normalizeVisibleText(expected)),
|
|
113
|
+
`слайд ${contract.number}: вложенный текст front matter «${expected}» не виден; `
|
|
114
|
+
+ `видимые Text=${JSON.stringify(normalized)}`,
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const overflow = await slide.evaluate(element => ({
|
|
119
|
+
x: element.scrollWidth - element.clientWidth,
|
|
120
|
+
y: element.scrollHeight - element.clientHeight,
|
|
121
|
+
}))
|
|
122
|
+
assert.ok(overflow.x <= 1, `слайд ${contract.number}: горизонтальное переполнение ${overflow.x}px`)
|
|
123
|
+
assert.ok(overflow.y <= 1, `слайд ${contract.number}: вертикальное переполнение ${overflow.y}px`)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
assert.deepEqual(diagnostics, [], diagnostics.join('\n'))
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
await browser.close()
|
|
130
|
+
await server.close()
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
cpSync(FIXTURE_ROOT, deckRoot, { recursive: true })
|
|
136
|
+
const themeInstallRoot = join(deckRoot, 'node_modules', 'slidev-theme-practicum')
|
|
137
|
+
const manifest = JSON.parse(readFileSync(join(PROJECT_ROOT, 'package.json'), 'utf8'))
|
|
138
|
+
|
|
139
|
+
mkdirSync(themeInstallRoot, { recursive: true })
|
|
140
|
+
cpSync(join(PROJECT_ROOT, 'package.json'), join(themeInstallRoot, 'package.json'))
|
|
141
|
+
for (const entry of manifest.files)
|
|
142
|
+
cpSync(join(PROJECT_ROOT, entry), join(themeInstallRoot, entry), { recursive: true })
|
|
143
|
+
|
|
144
|
+
const deckPath = join(deckRoot, 'slides.md')
|
|
145
|
+
const validation = spawnSync(process.execPath, [
|
|
146
|
+
join(themeInstallRoot, 'scripts', 'validate-deck.cjs'),
|
|
147
|
+
deckPath,
|
|
148
|
+
], {
|
|
149
|
+
cwd: deckRoot,
|
|
150
|
+
encoding: 'utf8',
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
assert.equal(
|
|
154
|
+
validation.status,
|
|
155
|
+
0,
|
|
156
|
+
validation.stderr || validation.stdout || validation.error?.message,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
const result = spawnSync(process.execPath, [
|
|
160
|
+
SLIDEV_CLI,
|
|
161
|
+
'build',
|
|
162
|
+
deckPath,
|
|
163
|
+
'--out',
|
|
164
|
+
outputRoot,
|
|
165
|
+
], {
|
|
166
|
+
cwd: deckRoot,
|
|
167
|
+
encoding: 'utf8',
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
if (result.error || result.status !== 0) {
|
|
171
|
+
process.stderr.write(result.stderr || result.stdout || result.error?.message || 'Сборка локального варианта завершилась без диагностики.\n')
|
|
172
|
+
process.exitCode = result.status || 1
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
const artifact = Buffer.concat(readArtifact(outputRoot)).toString()
|
|
176
|
+
|
|
177
|
+
if (!artifact.includes('LocalLayoutVariantBadge')) {
|
|
178
|
+
console.error('Сборка не содержит локальный компонент, вызванный из варианта презентации.')
|
|
179
|
+
process.exitCode = 1
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
await inspectRenderedVariant()
|
|
183
|
+
console.log('Локальный вариант и вложенные поля front matter отрисовались в итоговой сборке установленной темы.')
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
rmSync(consumerRoot, { recursive: true, force: true })
|
|
189
|
+
}
|
|
@@ -83,12 +83,28 @@ try {
|
|
|
83
83
|
if (!files.has('LICENSE'))
|
|
84
84
|
errors.push('package должен содержать LICENSE')
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
for (const skillFile of [
|
|
87
|
+
'skills/slidev-practicum/SKILL.md',
|
|
88
|
+
'skills/slidev-practicum/references/deck-project-structure.md',
|
|
89
|
+
'skills/slidev-practicum/references/contour-illustrations.md',
|
|
90
|
+
'skills/slidev-practicum/references/photographic-illustrations.md',
|
|
91
|
+
]) {
|
|
92
|
+
if (!files.has(skillFile))
|
|
93
|
+
errors.push(`package должен содержать ${skillFile}`)
|
|
94
|
+
}
|
|
88
95
|
|
|
89
96
|
if (!files.has('example.md'))
|
|
90
97
|
errors.push('package должен содержать example.md как каноническую колоду для авторов')
|
|
91
98
|
|
|
99
|
+
for (const runtimeFile of [
|
|
100
|
+
'composables/local-layout-variant-files.ts',
|
|
101
|
+
'composables/local-layout-variants.ts',
|
|
102
|
+
'env.d.ts',
|
|
103
|
+
]) {
|
|
104
|
+
if (!files.has(runtimeFile))
|
|
105
|
+
errors.push(`package должен содержать контракт локальных вариантов: ${runtimeFile}`)
|
|
106
|
+
}
|
|
107
|
+
|
|
92
108
|
const defaultDeckTargets = new Set(
|
|
93
109
|
Object.values(manifest.scripts ?? {})
|
|
94
110
|
.flatMap(script => String(script).match(/\b[\w-]+\.md\b/g) ?? []),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Pillow==12.2.0
|