Skip to content

Commit 33398ce

Browse files
Merge branch 'main' into docs-file-imports
2 parents 5857214 + 20000e2 commit 33398ce

File tree

10 files changed

+217
-5
lines changed

10 files changed

+217
-5
lines changed

docs/tutorialkit.dev/src/content/docs/reference/configuration.mdx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,3 +403,47 @@ type OpenInStackBlitz =
403403
type TemplateType = "html" | "node" | "angular-cli" | "create-react-app" | "javascript" | "polymer" | "typescript" | "vue"
404404
405405
```
406+
407+
## Configure the Tutorialkit Astro integration
408+
409+
`@tutorialkit/astro` is an integration for Astro. You can configure the integration in your `astro.config.ts` file.
410+
411+
```ts "tutorialkit()" title="astro.config.ts"
412+
import tutorialkit from "@tutorialkit/astro";
413+
import { defineConfig } from "astro/config";
414+
415+
export default defineConfig({
416+
devToolbar: {
417+
enabled: false,
418+
},
419+
integrations: [
420+
tutorialkit(),
421+
],
422+
});
423+
```
424+
425+
You can pass the following options to the `tutorialkit` integration:
426+
427+
### `components`
428+
429+
**type**: `OverrideComponentsOptions`
430+
431+
Provide the path to the components you want to override.
432+
433+
```ts
434+
tutorialkit({
435+
components: {
436+
TopBar: './src/components/CustomTopBar.astro',
437+
},
438+
});
439+
```
440+
441+
See [Overriding Components](/guides/overriding-components/) for details of all the components that you can override.
442+
443+
### `defaultRoutes`
444+
445+
**type**: `boolean | "tutorial-only"`<br/>
446+
**default**: `true`
447+
448+
Controls whether the tutorial routes are automatically added to your project. When set to `true`, it additionally adds a redirect from `/` to the first tutorial.
449+
Use `"tutorial-only"` to only add the tutorial routes without the redirect.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
---
22
type: chapter
3-
title: filesystem
3+
title: File system
44
---
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
type: lesson
3+
title: Page one
4+
---
5+
6+
# Lesson order test - Page one
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
type: lesson
3+
title: Page two
4+
---
5+
6+
# Lesson order test - Page two
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
type: lesson
3+
title: Page three
4+
---
5+
6+
# Lesson order test - Page three
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
type: chapter
3+
title: Lesson order
4+
lessons:
5+
- 2-lesson
6+
- 3-lesson
7+
- 1-lesson
8+
---

e2e/test/lesson-order.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
const BASE_URL = '/tests/lesson-order';
4+
5+
test('developer can configure custom order for lessons', async ({ page }) => {
6+
await page.goto(`${BASE_URL}/1-lesson`);
7+
await expect(page.getByRole('heading', { level: 1, name: 'Lesson order test - Page one' })).toBeVisible();
8+
9+
// navigation select can take a while to hydrate on page load, click until responsive
10+
await expect(async () => {
11+
const button = page.getByRole('button', { name: 'Tests / Lesson order / Page one' });
12+
await button.click();
13+
await expect(page.locator('[data-state="open"]', { has: button })).toBeVisible({ timeout: 50 });
14+
}).toPass();
15+
16+
const list = page.getByRole('region', { name: 'Lesson order' });
17+
18+
// configured ordered is [2, 3, 1]
19+
await expect(list.getByRole('listitem').nth(0)).toHaveText('Page two');
20+
await expect(list.getByRole('listitem').nth(1)).toHaveText('Page three');
21+
await expect(list.getByRole('listitem').nth(2)).toHaveText('Page one');
22+
});

e2e/test/navigation.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ test('user can navigate between lessons using nav bar links', async ({ page }) =
2323
test('user can navigate between lessons using breadcrumbs', async ({ page }) => {
2424
await page.goto(`${BASE_URL}/page-one`);
2525

26-
await page.getByRole('button', { name: 'Tests / Navigation / Page one' }).click({ force: true });
26+
// navigation select can take a while to hydrate on page load, click until responsive
27+
await expect(async () => {
28+
const button = page.getByRole('button', { name: 'Tests / Navigation / Page one' });
29+
await button.click();
30+
await expect(page.locator('[data-state="open"]', { has: button })).toBeVisible({ timeout: 50 });
31+
}).toPass();
32+
2733
await page.getByRole('region', { name: 'Navigation' }).getByRole('link', { name: 'Page three' }).click();
2834

2935
await expect(page.getByRole('heading', { level: 1, name: 'Navigation test - Page three' })).toBeVisible();

e2e/test/terminal.test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,14 @@ test('user can open terminal', async ({ page }) => {
1515
// await expect(tab).not.toBeVisible();
1616
// await expect(panel).not.toBeVisible();
1717

18-
await page.getByRole('button', { name: 'Toggle Terminal' }).click();
18+
// terminal toggle can take a while to hydrate on page load, click until responsive
19+
await expect(async () => {
20+
await page.getByRole('button', { name: 'Toggle Terminal' }).click();
21+
22+
await expect(tab).toBeVisible({ timeout: 100 });
23+
await expect(panel).toBeVisible({ timeout: 100 });
24+
}).toPass();
1925

20-
await expect(tab).toBeVisible();
21-
await expect(panel).toBeVisible();
2226
await expect(panel).toContainText('~/tutorial', { useInnerText: true });
2327
});
2428

packages/astro/src/default/utils/content.spec.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as content from 'astro:content';
22
import { describe, expect, test, vi, type TaskContext } from 'vitest';
33
import { getTutorial, type CollectionEntryTutorial } from './content';
4+
import { logger } from './logger';
45

56
const getCollection = vi.mocked<() => Omit<CollectionEntryTutorial, 'render'>[]>(content.getCollection);
67
vi.mock('astro:content', () => ({ getCollection: vi.fn() }));
@@ -11,6 +12,8 @@ vi.mock(import('@tutorialkit/types'), async (importOriginal) => ({
1112
DEFAULT_LOCALIZATION: { mocked: 'default localization' } as any,
1213
}));
1314

15+
vi.mock(import('./logger'), async (importOriginal) => importOriginal());
16+
1417
expect.addSnapshotSerializer({
1518
serialize: (val) => JSON.stringify(val, null, 2),
1619
test: (value) => !(value instanceof Error),
@@ -95,6 +98,34 @@ test('multiple parts', async (ctx) => {
9598
await expect(collection).toMatchFileSnapshot(snapshotName(ctx));
9699
});
97100

101+
test('lessons with identical names in different chapters', async () => {
102+
getCollection.mockReturnValueOnce([
103+
{ id: 'meta.md', ...tutorial },
104+
{ id: '1-part/meta.md', ...part },
105+
106+
{ id: '1-part/1-chapter/meta.md', ...chapter },
107+
{
108+
id: '1-part/1-chapter/identical-lesson-name/content.md',
109+
...lesson,
110+
data: { ...lesson.data, focus: '/first.js' },
111+
},
112+
113+
{ id: '1-part/2-chapter/meta.md', ...chapter },
114+
{
115+
id: '1-part/2-chapter/identical-lesson-name/content.md',
116+
...lesson,
117+
data: { ...lesson.data, focus: '/second.js' },
118+
},
119+
]);
120+
121+
const collection = await getTutorial();
122+
const chapters = collection.parts['1-part'].chapters;
123+
124+
// verify that lesson.id is not used to define what makes a lesson unique (part.id + chapter.id too)
125+
expect(chapters['1-chapter'].lessons['identical-lesson-name']).toBeDefined();
126+
expect(chapters['2-chapter'].lessons['identical-lesson-name']).toBeDefined();
127+
});
128+
98129
describe('metadata inheriting', () => {
99130
test('lesson inherits metadata from tutorial', async () => {
100131
const data: CollectionEntryTutorial['data'] = {
@@ -212,6 +243,33 @@ describe('ordering', () => {
212243
expect(parts['2-part'].order).toBe(2);
213244
});
214245

246+
test('parts not mention in order are excluded ', async () => {
247+
vi.spyOn(logger, 'warn').mockImplementationOnce(vi.fn());
248+
249+
getCollection.mockReturnValueOnce([
250+
{
251+
id: 'meta.md',
252+
...tutorial,
253+
data: { ...tutorial.data, parts: ['2-part', '1-part'] },
254+
},
255+
{ id: '2-part/meta.md', ...part },
256+
{ id: 'excluded-part/meta.md', ...part },
257+
{ id: '1-part/meta.md', ...part },
258+
]);
259+
260+
const collection = await getTutorial();
261+
const parts = collection.parts;
262+
263+
expect(Object.keys(parts)).toHaveLength(2);
264+
expect(parts['excluded-part']).toBeUndefined();
265+
expect(parts['1-part']).toBeDefined();
266+
expect(parts['2-part']).toBeDefined();
267+
268+
expect(vi.mocked(logger.warn).mock.calls[0][0]).toMatchInlineSnapshot(
269+
`"An order was specified for the parts of the tutorial but 'excluded-part' is not included so it won't be visible."`,
270+
);
271+
});
272+
215273
test('chapters are ordered by default', async () => {
216274
getCollection.mockReturnValueOnce([
217275
{ id: 'meta.md', ...tutorial },
@@ -246,6 +304,30 @@ describe('ordering', () => {
246304
expect(chapters['2-chapter'].order).toBe(2);
247305
});
248306

307+
test('chapters not mention in order are excluded ', async () => {
308+
vi.spyOn(logger, 'warn').mockImplementationOnce(vi.fn());
309+
310+
getCollection.mockReturnValueOnce([
311+
{ id: 'meta.md', ...tutorial },
312+
{ id: '1-part/meta.md', ...part, data: { ...part.data, chapters: ['2-chapter', '1-chapter'] } },
313+
{ id: '1-part/2-chapter/meta.md', ...chapter },
314+
{ id: '1-part/excluded-chapter/meta.md', ...chapter },
315+
{ id: '1-part/1-chapter/meta.md', ...chapter },
316+
]);
317+
318+
const collection = await getTutorial();
319+
const chapters = collection.parts['1-part'].chapters;
320+
321+
expect(Object.keys(chapters)).toHaveLength(2);
322+
expect(chapters['excluded-part']).toBeUndefined();
323+
expect(chapters['1-chapter']).toBeDefined();
324+
expect(chapters['2-chapter']).toBeDefined();
325+
326+
expect(vi.mocked(logger.warn).mock.calls[0][0]).toMatchInlineSnapshot(
327+
`"An order was specified for part '1-part' but chapter 'excluded-chapter' is not included, so it won't be visible."`,
328+
);
329+
});
330+
249331
test('lessons are ordered by default', async () => {
250332
getCollection.mockReturnValueOnce([
251333
{ id: 'meta.md', ...tutorial },
@@ -288,6 +370,34 @@ describe('ordering', () => {
288370
expect(lessons['1-lesson'].order).toBe(1);
289371
expect(lessons['2-lesson'].order).toBe(2);
290372
});
373+
374+
test('lessons not mention in order are excluded ', async () => {
375+
vi.spyOn(logger, 'warn').mockImplementationOnce(vi.fn());
376+
377+
getCollection.mockReturnValueOnce([
378+
{ id: 'meta.md', ...tutorial },
379+
{ id: '1-part/meta.md', ...part },
380+
{
381+
id: '1-part/1-chapter/meta.md',
382+
...chapter,
383+
data: { ...chapter.data, lessons: ['2-lesson', '1-lesson'] },
384+
},
385+
{ id: '1-part/1-chapter/excluded-lesson/meta.md', ...lesson },
386+
{ id: '1-part/1-chapter/1-lesson/meta.md', ...lesson },
387+
{ id: '1-part/1-chapter/2-lesson/meta.md', ...lesson },
388+
]);
389+
390+
const collection = await getTutorial();
391+
const lessons = collection.parts['1-part'].chapters['1-chapter'].lessons;
392+
393+
expect(Object.keys(lessons)).toHaveLength(2);
394+
expect(lessons['1-lesson']).toBeDefined();
395+
expect(lessons['2-lesson']).toBeDefined();
396+
397+
expect(vi.mocked(logger.warn).mock.calls[0][0]).toMatchInlineSnapshot(
398+
`"An order was specified for chapter '1-chapter' but lesson 'excluded-lesson' is not included, so it won't be visible."`,
399+
);
400+
});
291401
});
292402

293403
describe('missing parts', () => {

0 commit comments

Comments
 (0)