From 5419038d1c0a6f80da4d9f31e330d0dc0e41def8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ari=20Perkki=C3=B6?= Date: Mon, 19 Aug 2024 14:57:02 +0300 Subject: [PATCH] feat(runtime): option for setting terminal open by default (#246) --- .../content/docs/reference/configuration.mdx | 6 +++- .../react/src/Panels/TerminalPanel.tsx | 6 ++++ .../react/src/Panels/WorkspacePanel.tsx | 4 +++ .../react/src/core/Terminal/index.tsx | 9 +++--- .../src/webcontainer/terminal-config.ts | 8 +++++ packages/types/src/schemas/common.ts | 2 ++ .../tests/terminal/default/content.md | 8 +++++ .../content/tutorial/tests/terminal/meta.md | 4 +++ .../tests/terminal/open-by-default/content.md | 9 ++++++ test/ui/test/terminal.test.ts | 32 +++++++++++++++++++ 10 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 test/ui/src/content/tutorial/tests/terminal/default/content.md create mode 100644 test/ui/src/content/tutorial/tests/terminal/meta.md create mode 100644 test/ui/src/content/tutorial/tests/terminal/open-by-default/content.md create mode 100644 test/ui/test/terminal.test.ts diff --git a/docs/tutorialkit.dev/src/content/docs/reference/configuration.mdx b/docs/tutorialkit.dev/src/content/docs/reference/configuration.mdx index ee8b6e1b..b72598aa 100644 --- a/docs/tutorialkit.dev/src/content/docs/reference/configuration.mdx +++ b/docs/tutorialkit.dev/src/content/docs/reference/configuration.mdx @@ -148,6 +148,8 @@ Configures one or more terminals. TutorialKit provides two types of terminals: r You can define which terminal panel will be active by default by specifying the `activePanel` value. The value is the given terminal's position in the `panels` array. If you omit the `activePanel` property, the first panel will be the active one. +You can set terminal open by default by specifying the `open` value. + An interactive terminal will disable the output redirect syntax by default. For instance, you cannot create a file `world.txt` with the contents `hello` using the command `echo hello > world.txt`. The reason is that this could disrupt the lesson if a user overwrites certain files. To allow output redirection, you can change the behavior with the `allowRedirects` setting. You can define this setting either per panel or for all panels at once. Additionally, you may not want users to run arbitrary commands. For example, if you are creating a lesson about `vitest`, you could specify that the only command the user can run is `vitest` by providing a list of `allowCommands`. Any other command executed by the user will be blocked. You can define the `allowCommands` setting either per panel or for all panels at once. @@ -162,7 +164,8 @@ type Terminal = { panels: TerminalPanel[], activePanel?: number, allowRedirects?: boolean, - allowCommands?: string[] + allowCommands?: string[], + open?: boolean, } type TerminalPanel = TerminalType @@ -177,6 +180,7 @@ Example value: ```yaml terminal: + open: true activePanel: 1 panels: - ['output', 'Dev Server'] diff --git a/packages/components/react/src/Panels/TerminalPanel.tsx b/packages/components/react/src/Panels/TerminalPanel.tsx index dee73b93..d0369a54 100644 --- a/packages/components/react/src/Panels/TerminalPanel.tsx +++ b/packages/components/react/src/Panels/TerminalPanel.tsx @@ -70,7 +70,10 @@ export function TerminalPanel({ theme, tutorialStore }: TerminalPanelProps) { }, )} title={title} + id={`tk-terminal-tab-${index}`} + role="tab" aria-selected={selected} + aria-controls={`tk-terminal-tapbanel-${index}`} onClick={() => setTabIndex(index)} > ( unsubscribe(); }, [tutorialStore.ref]); diff --git a/packages/components/react/src/core/Terminal/index.tsx b/packages/components/react/src/core/Terminal/index.tsx index 5d82ad82..1a0c0058 100644 --- a/packages/components/react/src/core/Terminal/index.tsx +++ b/packages/components/react/src/core/Terminal/index.tsx @@ -2,7 +2,7 @@ import { FitAddon } from '@xterm/addon-fit'; import { WebLinksAddon } from '@xterm/addon-web-links'; import { Terminal as XTerm } from '@xterm/xterm'; import '@xterm/xterm/css/xterm.css'; -import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'; +import { forwardRef, useEffect, useImperativeHandle, useRef, type ComponentProps } from 'react'; import '../../styles/terminal.css'; import { getTerminalTheme } from './theme.js'; @@ -10,16 +10,15 @@ export interface TerminalRef { reloadStyles: () => void; } -export interface TerminalProps { +export interface TerminalProps extends ComponentProps<'div'> { theme: 'dark' | 'light'; - className?: string; readonly?: boolean; onTerminalReady?: (terminal: XTerm) => void; onTerminalResize?: (cols: number, rows: number) => void; } export const Terminal = forwardRef( - ({ theme, className = '', readonly = true, onTerminalReady, onTerminalResize }, ref) => { + ({ theme, readonly = true, onTerminalReady, onTerminalResize, ...props }, ref) => { const divRef = useRef(null); const terminalRef = useRef(); @@ -79,7 +78,7 @@ export const Terminal = forwardRef( }; }, []); - return
; + return
; }, ); diff --git a/packages/runtime/src/webcontainer/terminal-config.ts b/packages/runtime/src/webcontainer/terminal-config.ts index b188c85c..60325878 100644 --- a/packages/runtime/src/webcontainer/terminal-config.ts +++ b/packages/runtime/src/webcontainer/terminal-config.ts @@ -5,6 +5,7 @@ import type { ITerminal } from '../utils/terminal.js'; interface NormalizedTerminalConfig { panels: TerminalPanel[]; activePanel: number; + defaultOpen: boolean; } interface TerminalPanelOptions { @@ -30,6 +31,10 @@ export class TerminalConfig { get activePanel() { return this._config.activePanel; } + + get defaultOpen() { + return this._config.defaultOpen; + } } const TERMINAL_PANEL_TITLES: Record = { @@ -192,6 +197,7 @@ function normalizeTerminalConfig(config?: TerminalSchema): NormalizedTerminalCon return { panels: [], activePanel, + defaultOpen: false, }; } @@ -203,6 +209,7 @@ function normalizeTerminalConfig(config?: TerminalSchema): NormalizedTerminalCon return { panels: [new TerminalPanel('output')], activePanel, + defaultOpen: false, }; } @@ -254,5 +261,6 @@ function normalizeTerminalConfig(config?: TerminalSchema): NormalizedTerminalCon return { activePanel, panels, + defaultOpen: config.open || false, }; } diff --git a/packages/types/src/schemas/common.ts b/packages/types/src/schemas/common.ts index 56c56c2c..98698db2 100644 --- a/packages/types/src/schemas/common.ts +++ b/packages/types/src/schemas/common.ts @@ -74,6 +74,8 @@ export const terminalSchema = z.union([ z.boolean(), z.strictObject({ + open: z.boolean().optional().describe('Defines if terminal should be open by default'), + panels: z.union([ // either literally just `output` z.literal('output'), diff --git a/test/ui/src/content/tutorial/tests/terminal/default/content.md b/test/ui/src/content/tutorial/tests/terminal/default/content.md new file mode 100644 index 00000000..7ffed56d --- /dev/null +++ b/test/ui/src/content/tutorial/tests/terminal/default/content.md @@ -0,0 +1,8 @@ +--- +type: lesson +title: Default +terminal: + panels: terminal +--- + +# Terminal test - Default diff --git a/test/ui/src/content/tutorial/tests/terminal/meta.md b/test/ui/src/content/tutorial/tests/terminal/meta.md new file mode 100644 index 00000000..6500e742 --- /dev/null +++ b/test/ui/src/content/tutorial/tests/terminal/meta.md @@ -0,0 +1,4 @@ +--- +type: chapter +title: Terminal +--- diff --git a/test/ui/src/content/tutorial/tests/terminal/open-by-default/content.md b/test/ui/src/content/tutorial/tests/terminal/open-by-default/content.md new file mode 100644 index 00000000..834ff796 --- /dev/null +++ b/test/ui/src/content/tutorial/tests/terminal/open-by-default/content.md @@ -0,0 +1,9 @@ +--- +type: lesson +title: Open by default +terminal: + open: true + panels: "terminal" +--- + +# Terminal test - Open by default diff --git a/test/ui/test/terminal.test.ts b/test/ui/test/terminal.test.ts new file mode 100644 index 00000000..a75744e0 --- /dev/null +++ b/test/ui/test/terminal.test.ts @@ -0,0 +1,32 @@ +import { test, expect } from '@playwright/test'; + +const BASE_URL = '/tests/terminal'; + +test('user can open terminal', async ({ page }) => { + await page.goto(`${BASE_URL}/default`); + + await expect(page.getByRole('heading', { level: 1, name: 'Terminal test - Default' })).toBeVisible(); + + const tab = page.getByRole('tab', { name: 'Terminal' }); + const panel = page.getByRole('tabpanel', { name: 'Terminal' }); + + /* eslint-disable multiline-comment-style */ + // TODO: Requires #245 + // await expect(tab).not.toBeVisible(); + // await expect(panel).not.toBeVisible(); + + await page.getByRole('button', { name: 'Toggle Terminal' }).click(); + + await expect(tab).toBeVisible(); + await expect(panel).toBeVisible(); + await expect(panel).toContainText('~/tutorial', { useInnerText: true }); +}); + +test('user can see terminal open by default', async ({ page }) => { + await page.goto(`${BASE_URL}/open-by-default`); + + await expect(page.getByRole('heading', { level: 1, name: 'Terminal test - Open by default' })).toBeVisible(); + + await expect(page.getByRole('tab', { name: 'Terminal', selected: true })).toBeVisible(); + await expect(page.getByRole('tabpanel', { name: 'Terminal' })).toContainText('~/tutorial', { useInnerText: true }); +});