Skip to content

feat(astro): support lessons without parts or chapters #374

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions docs/tutorialkit.dev/src/content/docs/guides/creating-content.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title: Content creation
description: 'Creating content in TutorialKit.'
---
import { FileTree } from '@astrojs/starlight/components';
import { Tabs, TabItem } from '@astrojs/starlight/components';

From an information architecture perspective, tutorial content is divided into **parts**, which are further divided into **chapters**, each consisting of **lessons**.

Expand Down Expand Up @@ -36,6 +37,64 @@ This structure is reflected in the directory structure of your TutorialKit proje

Navigate into one of these folders to see another folder that represents a **chapter**. Inside the chapter folder, you will find one or more **lesson** folders.

You can also omit parts or chapters such that you only have lessons or only lessons and parts. Here are a few examples:

<Tabs>
<TabItem label="Structure">
```plaintext
- Lesson 1: Getting started
- Lesson 2: Adding pages
```
</TabItem>

<TabItem label="File tree">
<FileTree>
- src
- content
- tutorial
- getting-started
- _files/
- _solution/
- content.md
- adding-pages/
- meta.md
- config.ts
- templates/
</FileTree>
</TabItem>
</Tabs>

<Tabs>
<TabItem label="Structure">
```plaintext
- Part 1: Introduction
- Lesson 1: What is Vite?
- Lesson 2: Installing
- …
- Part 2: Project structure
- …
```
</TabItem>

<TabItem label="File tree">
<FileTree>
- src
- content
- tutorial
- introduction/
- what-is-vite/
- _files/
- _solution/
- content.md
- installing/
- project-structure/
- meta.md
- config.ts
- templates/
</FileTree>
</TabItem>
</Tabs>

## A lesson content file

Navigate to the `src/content/tutorial/1-basics/1-introduction/1-welcome` folder and open the `content.md` in your editor. You will see a file structured like this:
Expand Down
10 changes: 10 additions & 0 deletions e2e/configs/lessons-in-part.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import tutorialkit from '@tutorialkit/astro';
import { defineConfig } from 'astro/config';

export default defineConfig({
devToolbar: { enabled: false },
server: { port: 4332 },
outDir: './dist-lessons-in-part',
integrations: [tutorialkit()],
srcDir: './src-custom/lessons-in-part',
});
10 changes: 10 additions & 0 deletions e2e/configs/lessons-in-root.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import tutorialkit from '@tutorialkit/astro';
import { defineConfig } from 'astro/config';

export default defineConfig({
devToolbar: { enabled: false },
server: { port: 4331 },
outDir: './dist-lessons-in-root',
integrations: [tutorialkit()],
srcDir: './src-custom/lessons-in-root',
});
4 changes: 4 additions & 0 deletions e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
"preview": "astro build && astro preview",
"dev:override-components": "astro dev --config ./configs/override-components.ts",
"preview:override-components": "astro build --config ./configs/override-components.ts && astro preview --config ./configs/override-components.ts",
"dev:lessons-in-root": "astro dev --config ./configs/lessons-in-root.ts",
"preview:lessons-in-root": "astro build --config ./configs/lessons-in-root.ts && astro preview --config ./configs/lessons-in-root.ts",
"dev:lessons-in-part": "astro dev --config ./configs/lessons-in-part.ts",
"preview:lessons-in-part": "astro build --config ./configs/lessons-in-part.ts && astro preview --config ./configs/lessons-in-part.ts",
Comment on lines +10 to +13
Copy link
Member Author

@AriPerkkio AriPerkkio Oct 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm planning to create scripts/start.mjs that can be called like node scripts/start.mjs --dev lessons-in-root to reduce these verbose scripts. But that will be added in follow-up PR.

"test": "playwright test",
"test:ui": "pnpm run test --ui"
},
Expand Down
40 changes: 33 additions & 7 deletions e2e/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,59 @@
import { defineConfig } from '@playwright/test';

const serverOptions = {
reuseExistingServer: !process.env.CI,
stdout: 'ignore',
stderr: 'pipe',
} as const;

export default defineConfig({
projects: [
{
name: 'Default',
testMatch: 'test/*.test.ts',
testIgnore: 'test/*.override-components.test.ts',
testIgnore: [
'test/*.override-components.test.ts',
'test/*.lessons-in-part.test.ts',
'test/*.lessons-in-root.test.ts',
],
use: { baseURL: 'http://localhost:4329' },
},
{
name: 'Override Components',
testMatch: 'test/*.override-components.test.ts',
use: { baseURL: 'http://localhost:4330' },
},
{
name: 'Lessons in root',
testMatch: 'test/*.lessons-in-root.test.ts',
use: { baseURL: 'http://localhost:4331' },
},
{
name: 'Lessons in part',
testMatch: 'test/*.lessons-in-part.test.ts',
use: { baseURL: 'http://localhost:4332' },
},
],
webServer: [
{
command: 'pnpm preview',
url: 'http://localhost:4329',
reuseExistingServer: !process.env.CI,
stdout: 'ignore',
stderr: 'pipe',
...serverOptions,
},
{
command: 'pnpm preview:override-components',
url: 'http://localhost:4330',
reuseExistingServer: !process.env.CI,
stdout: 'ignore',
stderr: 'pipe',
...serverOptions,
},
{
command: 'pnpm preview:lessons-in-root',
url: 'http://localhost:4331',
...serverOptions,
},
{
command: 'pnpm preview:lessons-in-part',
url: 'http://localhost:4332',
...serverOptions,
},
],
expect: {
Expand Down
9 changes: 9 additions & 0 deletions e2e/src-custom/lessons-in-part/content/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { contentSchema } from '@tutorialkit/types';
import { defineCollection } from 'astro:content';

const tutorial = defineCollection({
type: 'content',
schema: contentSchema,
});

export const collections = { tutorial };
5 changes: 5 additions & 0 deletions e2e/src-custom/lessons-in-part/content/tutorial/meta.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
type: tutorial
mainCommand: ''
prepareCommands: []
---
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
type: lesson
title: Lesson one
---

# Lessons in part test - Lesson one

Lesson in part without chapter
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
type: lesson
title: Lesson two
---

# Lessons in part test - Lesson two

Lesson in part without chapter
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
type: part
title: 'Part one'
---
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
type: lesson
title: Lesson three
---

# Lessons in part test - Lesson three

Lesson in chapter
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
type: lesson
title: Lesson four
---

# Lessons in part test - Lesson four

Lesson in chapter
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
type: chapter
title: 'Chapter one'
---
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
type: part
title: 'Part two'
---
3 changes: 3 additions & 0 deletions e2e/src-custom/lessons-in-part/env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/// <reference path="../../.astro/types.d.ts" />
/// <reference types="@tutorialkit/astro/types" />
/// <reference types="astro/client" />
9 changes: 9 additions & 0 deletions e2e/src-custom/lessons-in-root/content/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { contentSchema } from '@tutorialkit/types';
import { defineCollection } from 'astro:content';

const tutorial = defineCollection({
type: 'content',
schema: contentSchema,
});

export const collections = { tutorial };
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
type: lesson
title: Lesson one
---

# Lessons in root test - Lesson one

Lesson in root without part
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
type: lesson
title: Lesson two
---

# Lessons in root test - Lesson two

Lesson in root without part
5 changes: 5 additions & 0 deletions e2e/src-custom/lessons-in-root/content/tutorial/meta.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
type: tutorial
mainCommand: ''
prepareCommands: []
---
3 changes: 3 additions & 0 deletions e2e/src-custom/lessons-in-root/env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/// <reference path="../../.astro/types.d.ts" />
/// <reference types="@tutorialkit/astro/types" />
/// <reference types="astro/client" />
62 changes: 62 additions & 0 deletions e2e/test/navigation.lessons-in-part.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { test, expect } from '@playwright/test';

test('user can navigate between lessons using breadcrumbs', async ({ page }) => {
await page.goto('/');

await expect(page.getByRole('heading', { level: 1, name: 'Lessons in part test - Lesson one' })).toBeVisible();
await expect(page.getByText('Lesson in part without chapter')).toBeVisible();

// navigation select can take a while to hydrate on page load, click until responsive
await expect(async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh interesting, what does this expect call do (compared to having them in the parent function)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The button can become visible before it has been hydrated. Clicking it doesn't do anything. So here if the last await expect(page.locator('[data-state="open"]', { has: button })).toBeVisible({ timeout: 50 }) fails, the whole callback inside expect(callback).toPass() will be retried -> button is clicked again.

This hydration issue might be related to running <root>/e2e in dev mode only. I'll try in follow-up PR that if we switch to vite build --watch & vite preview, would this issue disappear.

const button = page.getByRole('button', { name: 'Part one / Lesson one' });
await button.click();
await expect(page.locator('[data-state="open"]', { has: button })).toBeVisible({ timeout: 50 });
}).toPass();

const navigation = page.getByRole('navigation');
await navigation.getByRole('region', { name: 'Part 1: Part one' }).getByRole('link', { name: 'Lesson two' }).click();

await expect(page.getByRole('heading', { level: 1, name: 'Lessons in part test - Lesson two' })).toBeVisible();
await expect(page.getByText('Lesson in part without chapter')).toBeVisible();

await expect(async () => {
const button = page.getByRole('button', { name: 'Part one / Lesson two' });
await button.click();
await expect(page.locator('[data-state="open"]', { has: button })).toBeVisible({ timeout: 50 });
}).toPass();

// expand part
await navigation.getByRole('button', { name: 'Part 2: Part two' }).click();

// expand chapter
await navigation
.getByRole('region', { name: 'Part 2: Part two' })
.getByRole('button', { name: 'Chapter one' })
.click();

// select lesson
await navigation.getByRole('region', { name: 'Chapter one' }).getByRole('link', { name: 'Lesson three' }).click();

await expect(page.getByRole('heading', { level: 1, name: 'Lessons in part test - Lesson three' })).toBeVisible();
await expect(page.getByText('Lesson in chapter')).toBeVisible();
});

test('user can navigate between lessons using nav bar links', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('heading', { level: 1, name: 'Lessons in part test - Lesson one' })).toBeVisible();
await expect(page.getByText('Lesson in part without chapter')).toBeVisible();

await navigateToPage('Lesson two');
await expect(page.getByText('Lesson in part without chapter')).toBeVisible();

await navigateToPage('Lesson three');
await expect(page.getByText('Lesson in chapter')).toBeVisible();

await navigateToPage('Lesson four');
await expect(page.getByText('Lesson in chapter')).toBeVisible();

async function navigateToPage(title: string) {
await page.getByRole('link', { name: title }).click();
await expect(page.getByRole('heading', { level: 1, name: `Lessons in part test - ${title}` })).toBeVisible();
}
});
19 changes: 19 additions & 0 deletions e2e/test/navigation.lessons-in-root.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { test, expect } from '@playwright/test';

test('user can navigate between lessons using breadcrumbs', async ({ page }) => {
await page.goto('/lesson-one');

await expect(page.getByRole('heading', { level: 1, name: 'Lessons in root test - Lesson one' })).toBeVisible();
await expect(page.getByText('Lesson in root without part')).toBeVisible();

// navigation select can take a while to hydrate on page load, click until responsive
await expect(async () => {
const button = page.getByRole('button', { name: 'Lesson one' });
await button.click();
await expect(page.locator('[data-state="open"]', { has: button })).toBeVisible({ timeout: 50 });
}).toPass();

await page.getByRole('navigation').getByRole('link', { name: 'Lesson two' }).click();

await expect(page.getByRole('heading', { level: 1, name: 'Lessons in root test - Lesson two' })).toBeVisible();
});
10 changes: 6 additions & 4 deletions packages/astro/src/default/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import { joinPaths } from '../utils/url';

const tutorial = await getTutorial();

const part = tutorial.parts[tutorial.firstPartId!];
const chapter = part.chapters[part?.firstChapterId!];
const lesson = chapter.lessons[chapter?.firstLessonId!];
const lesson = tutorial.lessons[0];
const part = lesson.part && tutorial.parts[lesson.part.id];
const chapter = lesson.chapter && part?.chapters[lesson.chapter.id];

const redirect = joinPaths(import.meta.env.BASE_URL, `/${part.slug}/${chapter.slug}/${lesson.slug}`);
const slug = [part?.slug, chapter?.slug, lesson.slug].filter(Boolean).join('/');

const redirect = joinPaths(import.meta.env.BASE_URL, `/${slug}`);
---

<!doctype html>
Expand Down
Loading