jbilcke-hf HF staff commited on
Commit
24c6b45
1 Parent(s): 0f737ff

setup unit and e2e tests

Browse files
.github/workflows/playwright.yml ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Playwright Tests
2
+ on:
3
+ push:
4
+ branches: [ main, master ]
5
+ pull_request:
6
+ branches: [ main, master ]
7
+ jobs:
8
+ test:
9
+ timeout-minutes: 60
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-node@v4
14
+ with:
15
+ node-version: lts/*
16
+ - name: Install dependencies
17
+ run: npm ci
18
+ - name: Install Playwright Browsers
19
+ run: npx playwright install --with-deps
20
+ - name: Run Playwright tests
21
+ run: npx playwright test
22
+ - uses: actions/upload-artifact@v4
23
+ if: always()
24
+ with:
25
+ name: playwright-report
26
+ path: playwright-report/
27
+ retention-days: 30
.gitignore CHANGED
@@ -35,4 +35,8 @@ yarn-error.log*
35
  *.tsbuildinfo
36
  next-env.d.ts
37
 
38
- /sandbox/
 
 
 
 
 
35
  *.tsbuildinfo
36
  next-env.d.ts
37
 
38
+ /sandbox/
39
+ /test-results/
40
+ /playwright-report/
41
+ /blob-report/
42
+ /playwright/.cache/
README.md CHANGED
@@ -100,7 +100,41 @@ I haven't setup Prettier or a Linter yet.
100
 
101
  ### Testing
102
 
103
- There are no automated tests yet, @jbilcke-hf will act as the "QA engineer".
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
  ### Architecture
106
 
 
100
 
101
  ### Testing
102
 
103
+ @jbilcke-hf is working to add tests, and will also act as the "QA engineer".
104
+
105
+ #### Unit tests
106
+
107
+ Note: I've just added Vitest, we don't have tests yet.
108
+
109
+ `npm run test`
110
+
111
+ #### End-to-end tests
112
+
113
+
114
+ Note: I've just added Playwright, we don't have tests yet.
115
+
116
+ `npx playwright test`
117
+ Runs the end-to-end tests.
118
+
119
+ `npx playwright test --ui`
120
+ Starts the interactive UI mode.
121
+
122
+ `npx playwright test --project=chromium`
123
+ Runs the tests only on Desktop Chrome.
124
+
125
+ `npx playwright test example`
126
+ Runs the tests in a specific file.
127
+
128
+ ` npx playwright test --debug`
129
+ Runs the tests in debug mode.
130
+
131
+ `npx playwright codegen`
132
+ Auto generate tests with Codegen.
133
+
134
+ We suggest that you begin by typing:
135
+
136
+ `npx playwright test`
137
+
138
 
139
  ### Architecture
140
 
package-lock.json CHANGED
The diff for this file is too large to render. See raw diff
 
package.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "@aitube/clapper",
3
- "version": "0.0.4",
4
  "private": true,
5
  "description": "🎬 Clapper",
6
  "scripts": {
@@ -8,12 +8,14 @@
8
  "build": "npm i && next build",
9
  "start": "next start",
10
  "lint": "next lint",
11
- "lint:fix": "next lint --fix"
 
 
12
  },
13
  "dependencies": {
14
  "@aitube/broadway": "0.0.22",
15
  "@aitube/clap": "0.0.30",
16
- "@aitube/clapper-services": "0.0.23",
17
  "@aitube/engine": "0.0.26",
18
  "@aitube/timeline": "0.0.42",
19
  "@fal-ai/serverless-client": "^0.11.0",
@@ -55,12 +57,14 @@
55
  "@react-three/uikit": "^0.3.4",
56
  "@react-three/uikit-lucide": "^0.3.4",
57
  "@tailwindcss/container-queries": "^0.1.1",
 
58
  "@upstash/ratelimit": "^1.1.3",
59
  "@upstash/redis": "^1.31.1",
60
  "autoprefixer": "10.4.19",
61
  "class-variance-authority": "^0.7.0",
62
  "clsx": "^2.1.1",
63
  "cmdk": "^0.2.1",
 
64
  "fflate": "^0.8.2",
65
  "fluent-ffmpeg": "^2.1.3",
66
  "framer-motion": "11.1.7",
@@ -100,17 +104,22 @@
100
  "zx": "^8.1.3"
101
  },
102
  "devDependencies": {
 
 
103
  "@types/fluent-ffmpeg": "^2.1.24",
104
  "@types/is-hotkey": "^0.1.10",
105
  "@types/node": "^20",
106
  "@types/react": "^18",
107
  "@types/react-dom": "^18",
108
  "@types/uuid": "^9.0.8",
 
109
  "eslint": "^8",
110
  "eslint-config-next": "14.2.4",
 
111
  "postcss": "^8",
112
  "tailwind-scrollbar": "^3.1.0",
113
  "tailwindcss": "^3.4.3",
114
- "typescript": "^5"
 
115
  }
116
  }
 
1
  {
2
  "name": "@aitube/clapper",
3
+ "version": "0.0.5",
4
  "private": true,
5
  "description": "🎬 Clapper",
6
  "scripts": {
 
8
  "build": "npm i && next build",
9
  "start": "next start",
10
  "lint": "next lint",
11
+ "lint:fix": "next lint --fix",
12
+ "test:unit": "vitest",
13
+ "test:e2e": "npx playwright test"
14
  },
15
  "dependencies": {
16
  "@aitube/broadway": "0.0.22",
17
  "@aitube/clap": "0.0.30",
18
+ "@aitube/clapper-services": "0.0.25",
19
  "@aitube/engine": "0.0.26",
20
  "@aitube/timeline": "0.0.42",
21
  "@fal-ai/serverless-client": "^0.11.0",
 
57
  "@react-three/uikit": "^0.3.4",
58
  "@react-three/uikit-lucide": "^0.3.4",
59
  "@tailwindcss/container-queries": "^0.1.1",
60
+ "@types/dom-speech-recognition": "^0.0.4",
61
  "@upstash/ratelimit": "^1.1.3",
62
  "@upstash/redis": "^1.31.1",
63
  "autoprefixer": "10.4.19",
64
  "class-variance-authority": "^0.7.0",
65
  "clsx": "^2.1.1",
66
  "cmdk": "^0.2.1",
67
+ "date-fns": "^3.6.0",
68
  "fflate": "^0.8.2",
69
  "fluent-ffmpeg": "^2.1.3",
70
  "framer-motion": "11.1.7",
 
104
  "zx": "^8.1.3"
105
  },
106
  "devDependencies": {
107
+ "@playwright/test": "^1.45.1",
108
+ "@testing-library/react": "^16.0.0",
109
  "@types/fluent-ffmpeg": "^2.1.24",
110
  "@types/is-hotkey": "^0.1.10",
111
  "@types/node": "^20",
112
  "@types/react": "^18",
113
  "@types/react-dom": "^18",
114
  "@types/uuid": "^9.0.8",
115
+ "@vitejs/plugin-react": "^4.3.1",
116
  "eslint": "^8",
117
  "eslint-config-next": "14.2.4",
118
+ "jsdom": "^24.1.0",
119
  "postcss": "^8",
120
  "tailwind-scrollbar": "^3.1.0",
121
  "tailwindcss": "^3.4.3",
122
+ "typescript": "^5",
123
+ "vitest": "^2.0.2"
124
  }
125
  }
playwright.config.ts ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig, devices } from '@playwright/test';
2
+
3
+ /**
4
+ * Read environment variables from file.
5
+ * https://github.com/motdotla/dotenv
6
+ */
7
+ // import dotenv from 'dotenv';
8
+ // dotenv.config({ path: path.resolve(__dirname, '.env') });
9
+
10
+ /**
11
+ * See https://playwright.dev/docs/test-configuration.
12
+ */
13
+ export default defineConfig({
14
+ testDir: './tests',
15
+ /* Run tests in files in parallel */
16
+ fullyParallel: true,
17
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
18
+ forbidOnly: !!process.env.CI,
19
+ /* Retry on CI only */
20
+ retries: process.env.CI ? 2 : 0,
21
+ /* Opt out of parallel tests on CI. */
22
+ workers: process.env.CI ? 1 : undefined,
23
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
24
+ reporter: 'html',
25
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
26
+ use: {
27
+ /* Base URL to use in actions like `await page.goto('/')`. */
28
+ // baseURL: 'http://127.0.0.1:3000',
29
+
30
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
31
+ trace: 'on-first-retry',
32
+ },
33
+
34
+ /* Configure projects for major browsers */
35
+ projects: [
36
+ {
37
+ name: 'chromium',
38
+ use: { ...devices['Desktop Chrome'] },
39
+ },
40
+
41
+ {
42
+ name: 'firefox',
43
+ use: { ...devices['Desktop Firefox'] },
44
+ },
45
+
46
+ {
47
+ name: 'webkit',
48
+ use: { ...devices['Desktop Safari'] },
49
+ },
50
+
51
+ /* Test against mobile viewports. */
52
+ // {
53
+ // name: 'Mobile Chrome',
54
+ // use: { ...devices['Pixel 5'] },
55
+ // },
56
+ // {
57
+ // name: 'Mobile Safari',
58
+ // use: { ...devices['iPhone 12'] },
59
+ // },
60
+
61
+ /* Test against branded browsers. */
62
+ // {
63
+ // name: 'Microsoft Edge',
64
+ // use: { ...devices['Desktop Edge'], channel: 'msedge' },
65
+ // },
66
+ // {
67
+ // name: 'Google Chrome',
68
+ // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
69
+ // },
70
+ ],
71
+
72
+ /* Run your local dev server before starting the tests */
73
+ // webServer: {
74
+ // command: 'npm run start',
75
+ // url: 'http://127.0.0.1:3000',
76
+ // reuseExistingServer: !process.env.CI,
77
+ // },
78
+ });
src/components/toolbars/top-menu/assistant/index.tsx CHANGED
@@ -14,13 +14,13 @@ import {
14
  import { useUI } from "@/services/ui"
15
  import { SettingsCategory } from "@aitube/clapper-services"
16
  import { AssistantModelList } from "../lists/AssistantModelList"
17
- import { useInitAssistant } from "@/services/assistant/useAssistant"
18
 
19
  export function TopMenuAssistant() {
20
  const setShowSettings = useUI(s => s.setShowSettings)
21
 
22
  // this should only be called on and at only one place in the project!
23
- useInitAssistant()
24
 
25
  return (
26
  <MenubarMenu>
 
14
  import { useUI } from "@/services/ui"
15
  import { SettingsCategory } from "@aitube/clapper-services"
16
  import { AssistantModelList } from "../lists/AssistantModelList"
17
+ import { useVoiceAssistant } from "@/services/assistant/useVoiceAssistant"
18
 
19
  export function TopMenuAssistant() {
20
  const setShowSettings = useUI(s => s.setShowSettings)
21
 
22
  // this should only be called on and at only one place in the project!
23
+ useVoiceAssistant()
24
 
25
  return (
26
  <MenubarMenu>
src/lib/hf/adapter/findMainGradioEndpoint.ts CHANGED
@@ -23,9 +23,9 @@ export function findMainGradioEndpoint({
23
  const sortableEndpoints = endpoints.map(({ isNamed, name, endpoint, score }) => {
24
  console.log(`found endpoint: ${name}`)
25
 
26
- const isContinuous = !!endpoint.type?.continuous
27
- const isGenerator = !!endpoint.type?.generator
28
- const canCancel = !!endpoint.type?.cancel
29
 
30
  let gradioFields: Record<string, Partial<SupportedFields>> = {}
31
  let allGradioFields = getDefaultFields()
 
23
  const sortableEndpoints = endpoints.map(({ isNamed, name, endpoint, score }) => {
24
  console.log(`found endpoint: ${name}`)
25
 
26
+ // const isContinuous = !!endpoint.type?.continuous
27
+ // const isGenerator = !!endpoint.type?.generator
28
+ // const canCancel = !!endpoint.type?.cancel
29
 
30
  let gradioFields: Record<string, Partial<SupportedFields>> = {}
31
  let allGradioFields = getDefaultFields()
src/lib/utils/formatDuration.test.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { expect, test } from 'vitest'
2
+
3
+ import { formatDuration } from './formatDuration'
4
+
5
+ test('formatDuration', () => {
6
+ expect(formatDuration(0)).toBe('00:00:00:000')
7
+ expect(formatDuration(1050)).toBe('00:00:01:050')
8
+ expect(formatDuration(60500)).toBe('00:01:00:500')
9
+ expect(formatDuration(3600999)).toBe('01:00:00:999')
10
+ })
src/lib/utils/formatDuration.ts CHANGED
@@ -1,25 +1,27 @@
1
- export const HOUR = 60 * 60 * 1000; // Converted to milliseconds
2
- export const MINUTE = 60 * 1000; // Converted to milliseconds
3
 
4
  export function formatDuration(float_ms: number) {
5
 
6
- let float_s = float_ms / 1000; // Converted to seconds
7
-
8
- const hours = Math.floor(float_s / HOUR);
9
- float_s = float_s % HOUR;
10
- const minutes = Math.floor(float_s / MINUTE);
11
- float_s = float_s % MINUTE;
12
- const seconds = float_s;
13
 
14
- return `${twoDigits(hours)}:${twoDigits(minutes)}:${twoDigits(seconds, 3)}`;
15
- }
 
 
 
 
 
 
16
 
17
- export function twoDigits(number: number, decimal = 0) {
18
- let [int, dec] = number.toFixed(decimal).split(".");
19
- dec = dec || "";
20
- if (dec == "") {
21
- return int.padStart(2, "0");
22
- } else {
23
- return int.padStart(2, "0") + "." + dec;
24
- }
25
- }
 
 
 
1
+
2
+ import { intervalToDuration } from 'date-fns'
3
 
4
  export function formatDuration(float_ms: number) {
5
 
6
+ const duration = intervalToDuration({ start: 0, end: float_ms })
 
 
 
 
 
 
7
 
8
+ const hours = duration.hours || 0
9
+ const minutes = duration.minutes || 0
10
+ const seconds = duration.seconds || 0
11
+
12
+ const total =
13
+ (hours * 60 * 60 * 1000)
14
+ + (minutes * 60 * 1000)
15
+ + (seconds * 1000)
16
 
17
+ const formatted = [
18
+ duration.hours || 0,
19
+ duration.minutes || 0,
20
+ duration.seconds || 0,
21
+ float_ms - total
22
+ ]
23
+ .map((num, i) => String(num as number).padStart(i === 3 ? 3 : 2, '0'))
24
+ .join(':')
25
+
26
+ return formatted
27
+ }
src/services/assistant/getDefaultAssistantState.ts CHANGED
@@ -2,9 +2,6 @@ import { AssistantState } from "@aitube/clapper-services"
2
 
3
  export function getDefaultAssistantState(): AssistantState {
4
  const state: AssistantState = {
5
- isVoiceEnabled: false,
6
- transcript: "",
7
-
8
  history: [],
9
  }
10
 
 
2
 
3
  export function getDefaultAssistantState(): AssistantState {
4
  const state: AssistantState = {
 
 
 
5
  history: [],
6
  }
7
 
src/services/assistant/useAssistant.ts CHANGED
@@ -1,10 +1,8 @@
1
  "use client"
2
 
3
- import { useEffect } from "react"
4
- import { useVoiceToText } from "react-speakup"
5
  import { create } from "zustand"
6
  import { AssistantRequest, AssistantStore, ChatEvent } from "@aitube/clapper-services"
7
- import { ClapOutputType, ClapProject, ClapSegmentCategory, newSegment, UUID } from "@aitube/clap"
8
  import { DEFAULT_DURATION_IN_MS_PER_STEP, findFreeTrack, TimelineSegment, TimelineStore, useTimeline } from "@aitube/timeline"
9
 
10
  import { getDefaultAssistantState } from "./getDefaultAssistantState"
@@ -13,36 +11,11 @@ import { useSettings } from "../settings"
13
  import { askAssistant } from "./askAssistant"
14
  import { useRenderer } from "../renderer"
15
 
16
- // URL to the speech to text websocket server
17
- export const STT_API_URL = process.env.NEXT_PUBLIC_SPEECH_TO_TEXT_API_URL || ""
18
-
19
  const enableTextToSpeech = false
20
 
21
  export const useAssistant = create<AssistantStore>((set, get) => ({
22
  ...getDefaultAssistantState(),
23
 
24
- toggleVoice: (): boolean => {
25
-
26
- if (!navigator?.mediaDevices?.getUserMedia || !MediaRecorder.isTypeSupported("audio/webm")) {
27
- console.error("This environment doesn't support microphone recording")
28
- return false
29
- }
30
-
31
- if (!enableTextToSpeech) {
32
- console.error("Text to speech is currently disabled, aborting")
33
- return false
34
- }
35
-
36
- const isVoiceEnabled = !get().isVoiceEnabled
37
-
38
- set({ isVoiceEnabled })
39
-
40
- return isVoiceEnabled
41
- },
42
-
43
- setVoiceTranscript: (transcript: string) => {
44
- set({ transcript })
45
- },
46
  runCommand: (prompt: string) => {
47
 
48
  const query = prompt
@@ -256,26 +229,3 @@ export const useAssistant = create<AssistantStore>((set, get) => ({
256
  }
257
  }
258
  }))
259
-
260
- export function useInitAssistant() {
261
- const isVoiceEnabled = useAssistant(s => s.isVoiceEnabled)
262
- const toggleVoice = useAssistant(s => s.toggleVoice)
263
- const setVoiceTranscript = useAssistant(s => s.setVoiceTranscript)
264
- const { startListening, stopListening, transcript } = useVoiceToText({
265
- continuous: true
266
- })
267
-
268
- useEffect(() => {
269
- if (isVoiceEnabled) {
270
- console.log(`TODO: startListening`)
271
-
272
- startListening()
273
- } else {
274
- stopListening()
275
- }
276
- }, [isVoiceEnabled])
277
-
278
- useEffect(() => {
279
- setVoiceTranscript(transcript)
280
- }, [transcript])
281
- }
 
1
  "use client"
2
 
 
 
3
  import { create } from "zustand"
4
  import { AssistantRequest, AssistantStore, ChatEvent } from "@aitube/clapper-services"
5
+ import { ClapOutputType, ClapSegmentCategory, newSegment, UUID } from "@aitube/clap"
6
  import { DEFAULT_DURATION_IN_MS_PER_STEP, findFreeTrack, TimelineSegment, TimelineStore, useTimeline } from "@aitube/timeline"
7
 
8
  import { getDefaultAssistantState } from "./getDefaultAssistantState"
 
11
  import { askAssistant } from "./askAssistant"
12
  import { useRenderer } from "../renderer"
13
 
 
 
 
14
  const enableTextToSpeech = false
15
 
16
  export const useAssistant = create<AssistantStore>((set, get) => ({
17
  ...getDefaultAssistantState(),
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  runCommand: (prompt: string) => {
20
 
21
  const query = prompt
 
229
  }
230
  }
231
  }))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/services/assistant/useVoiceAssistant.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect } from "react"
2
+
3
+ import { useMic } from "../mic/useMic"
4
+ import { useAssistant } from "./useAssistant"
5
+
6
+ export function useVoiceAssistant() {
7
+ const processMessage = useAssistant(s => s.processMessage)
8
+ const transcript = useMic(s => s.transcript)
9
+
10
+ useEffect(() => {
11
+ processMessage(transcript)
12
+ }, [transcript])
13
+ }
src/services/debug.ts CHANGED
@@ -1,5 +1,6 @@
1
  import { useTasks } from "@/components/tasks/useTasks"
2
  import { useAssistant } from "./assistant/useAssistant"
 
3
  import { useAudio } from "./audio/useAudio"
4
  import { useBroadcast } from "./broadcast/useBroadcast"
5
  import { useEditors, useEntityEditor, useProjectEditor, useScriptEditor, useSegmentEditor } from "./editors"
@@ -18,6 +19,7 @@ if (typeof window !== "undefined") {
18
  const w = window as any
19
  w.useTasks = useTasks
20
  w.useAssistant = useAssistant
 
21
  w.useAudio = useAudio
22
  w.useBroadcast = useBroadcast
23
  w.useEditors = useEditors
 
1
  import { useTasks } from "@/components/tasks/useTasks"
2
  import { useAssistant } from "./assistant/useAssistant"
3
+ import { useMic } from "./mic/useMic"
4
  import { useAudio } from "./audio/useAudio"
5
  import { useBroadcast } from "./broadcast/useBroadcast"
6
  import { useEditors, useEntityEditor, useProjectEditor, useScriptEditor, useSegmentEditor } from "./editors"
 
19
  const w = window as any
20
  w.useTasks = useTasks
21
  w.useAssistant = useAssistant
22
+ w.useMic = useMic
23
  w.useAudio = useAudio
24
  w.useBroadcast = useBroadcast
25
  w.useEditors = useEditors
src/services/mic/getDefaultMicState.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { MicState } from "@aitube/clapper-services"
2
+
3
+ export function getDefaultMicState(): MicState {
4
+ return {
5
+ isSupported: typeof window === 'undefined' || !('webkitSpeechRecognition' in window),
6
+ isListening: false,
7
+ transcript: '',
8
+ interimResults: true,
9
+ error: '',
10
+ lang: 'en-US',
11
+ grammar: '#JSGF V1.0; grammer punctuation; public <punc> =. |, |? | | ; | : ;',
12
+ grammarWeight: 1,
13
+ continuous: false,
14
+ recognition: undefined,
15
+ }
16
+ }
src/services/mic/useMic.ts ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { create } from "zustand"
4
+ import { MicStore } from "@aitube/clapper-services"
5
+
6
+ import { getDefaultMicState } from "./getDefaultMicState"
7
+
8
+ export const useMic = create<MicStore>((set, get) => ({
9
+ ...getDefaultMicState(),
10
+
11
+ init: () => {
12
+ const { isSupported, interimResults, lang, continuous, grammar, grammarWeight } = get()
13
+
14
+ if (!isSupported) {
15
+ return
16
+ }
17
+
18
+ // Initialize webkitSpeechRecognition
19
+ const recognition: SpeechRecognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)()
20
+
21
+ if (!recognition) {
22
+ set({ isSupported: false, error: 'this browser doesn\'t support speech recognition' })
23
+ return
24
+ }
25
+
26
+ recognition.interimResults = interimResults
27
+ recognition.lang = lang
28
+ recognition.continuous = continuous
29
+
30
+ const speechRecognitionList = new window.webkitSpeechGrammarList()
31
+ speechRecognitionList.addFromString(grammar, grammarWeight)
32
+ recognition.grammars = speechRecognitionList
33
+
34
+ const handleResult = (event: SpeechRecognitionEvent) => {
35
+ let transcript = '';
36
+ for (let i = 0; i < event.results.length; i++) {
37
+ transcript += event.results?.[i]?.[0]?.transcript || '';
38
+ }
39
+ set({ transcript });
40
+ };
41
+
42
+ const handleError = (event: SpeechRecognitionErrorEvent) => {
43
+ let error = `${event.error} ${event.message})`
44
+
45
+ if (event.error === 'aborted') {
46
+ error = 'speech recognition aborted'
47
+ }
48
+ set({ isListening: false, error })
49
+ };
50
+
51
+ const handleEnd = () => {
52
+ set({ isListening: false, transcript: '' })
53
+ };
54
+
55
+ recognition.addEventListener('result', handleResult);
56
+ recognition.addEventListener('error', handleError);
57
+ recognition.addEventListener('end', handleEnd);
58
+
59
+ set({ recognition })
60
+ },
61
+ start: () => {
62
+ const { isSupported, recognition, isListening } = get()
63
+ if (!isSupported || !recognition || isListening) { return }
64
+ recognition.start()
65
+ set({ isListening: true, error: '' })
66
+ },
67
+ stop: () => {
68
+ const { isSupported, recognition, isListening } = get()
69
+ if (!isSupported || !recognition || !isListening) { return }
70
+ recognition.stop()
71
+ set({ isListening: false, error: '' })
72
+ },
73
+ clear: () => {
74
+ set({ transcript: '', error: '' })
75
+ },
76
+ }))
src/services/plugins/usePlugins.ts CHANGED
@@ -18,6 +18,7 @@ import { fetchAndRun } from "./fetchAndRun"
18
  import { useEditors, useEntityEditor, useProjectEditor, useSegmentEditor } from "../editors"
19
  import { useSimulator } from "../simulator/useSimulator"
20
  import { useIO } from "../io/useIO"
 
21
 
22
  export const usePlugins = create<PluginsStore>((set, get) => ({
23
  ...getDefaultPluginsState(),
@@ -49,6 +50,7 @@ export const usePlugins = create<PluginsStore>((set, get) => ({
49
  return {
50
  audio: useAudio,
51
  assistant: useAssistant,
 
52
  segmentEditor: useSegmentEditor,
53
  entityEditor: useEntityEditor,
54
  projectEditor: useProjectEditor,
 
18
  import { useEditors, useEntityEditor, useProjectEditor, useSegmentEditor } from "../editors"
19
  import { useSimulator } from "../simulator/useSimulator"
20
  import { useIO } from "../io/useIO"
21
+ import { useMic } from "../mic/useMic"
22
 
23
  export const usePlugins = create<PluginsStore>((set, get) => ({
24
  ...getDefaultPluginsState(),
 
50
  return {
51
  audio: useAudio,
52
  assistant: useAssistant,
53
+ mic: useMic,
54
  segmentEditor: useSegmentEditor,
55
  entityEditor: useEntityEditor,
56
  projectEditor: useProjectEditor,
tests/examples.spec.txt ADDED
@@ -0,0 +1,437 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { test, expect, type Page } from '@playwright/test';
2
+
3
+ test.beforeEach(async ({ page }) => {
4
+ await page.goto('https://demo.playwright.dev/todomvc');
5
+ });
6
+
7
+ const TODO_ITEMS = [
8
+ 'buy some cheese',
9
+ 'feed the cat',
10
+ 'book a doctors appointment'
11
+ ] as const;
12
+
13
+ test.describe('New Todo', () => {
14
+ test('should allow me to add todo items', async ({ page }) => {
15
+ // create a new todo locator
16
+ const newTodo = page.getByPlaceholder('What needs to be done?');
17
+
18
+ // Create 1st todo.
19
+ await newTodo.fill(TODO_ITEMS[0]);
20
+ await newTodo.press('Enter');
21
+
22
+ // Make sure the list only has one todo item.
23
+ await expect(page.getByTestId('todo-title')).toHaveText([
24
+ TODO_ITEMS[0]
25
+ ]);
26
+
27
+ // Create 2nd todo.
28
+ await newTodo.fill(TODO_ITEMS[1]);
29
+ await newTodo.press('Enter');
30
+
31
+ // Make sure the list now has two todo items.
32
+ await expect(page.getByTestId('todo-title')).toHaveText([
33
+ TODO_ITEMS[0],
34
+ TODO_ITEMS[1]
35
+ ]);
36
+
37
+ await checkNumberOfTodosInLocalStorage(page, 2);
38
+ });
39
+
40
+ test('should clear text input field when an item is added', async ({ page }) => {
41
+ // create a new todo locator
42
+ const newTodo = page.getByPlaceholder('What needs to be done?');
43
+
44
+ // Create one todo item.
45
+ await newTodo.fill(TODO_ITEMS[0]);
46
+ await newTodo.press('Enter');
47
+
48
+ // Check that input is empty.
49
+ await expect(newTodo).toBeEmpty();
50
+ await checkNumberOfTodosInLocalStorage(page, 1);
51
+ });
52
+
53
+ test('should append new items to the bottom of the list', async ({ page }) => {
54
+ // Create 3 items.
55
+ await createDefaultTodos(page);
56
+
57
+ // create a todo count locator
58
+ const todoCount = page.getByTestId('todo-count')
59
+
60
+ // Check test using different methods.
61
+ await expect(page.getByText('3 items left')).toBeVisible();
62
+ await expect(todoCount).toHaveText('3 items left');
63
+ await expect(todoCount).toContainText('3');
64
+ await expect(todoCount).toHaveText(/3/);
65
+
66
+ // Check all items in one call.
67
+ await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS);
68
+ await checkNumberOfTodosInLocalStorage(page, 3);
69
+ });
70
+ });
71
+
72
+ test.describe('Mark all as completed', () => {
73
+ test.beforeEach(async ({ page }) => {
74
+ await createDefaultTodos(page);
75
+ await checkNumberOfTodosInLocalStorage(page, 3);
76
+ });
77
+
78
+ test.afterEach(async ({ page }) => {
79
+ await checkNumberOfTodosInLocalStorage(page, 3);
80
+ });
81
+
82
+ test('should allow me to mark all items as completed', async ({ page }) => {
83
+ // Complete all todos.
84
+ await page.getByLabel('Mark all as complete').check();
85
+
86
+ // Ensure all todos have 'completed' class.
87
+ await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']);
88
+ await checkNumberOfCompletedTodosInLocalStorage(page, 3);
89
+ });
90
+
91
+ test('should allow me to clear the complete state of all items', async ({ page }) => {
92
+ const toggleAll = page.getByLabel('Mark all as complete');
93
+ // Check and then immediately uncheck.
94
+ await toggleAll.check();
95
+ await toggleAll.uncheck();
96
+
97
+ // Should be no completed classes.
98
+ await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']);
99
+ });
100
+
101
+ test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => {
102
+ const toggleAll = page.getByLabel('Mark all as complete');
103
+ await toggleAll.check();
104
+ await expect(toggleAll).toBeChecked();
105
+ await checkNumberOfCompletedTodosInLocalStorage(page, 3);
106
+
107
+ // Uncheck first todo.
108
+ const firstTodo = page.getByTestId('todo-item').nth(0);
109
+ await firstTodo.getByRole('checkbox').uncheck();
110
+
111
+ // Reuse toggleAll locator and make sure its not checked.
112
+ await expect(toggleAll).not.toBeChecked();
113
+
114
+ await firstTodo.getByRole('checkbox').check();
115
+ await checkNumberOfCompletedTodosInLocalStorage(page, 3);
116
+
117
+ // Assert the toggle all is checked again.
118
+ await expect(toggleAll).toBeChecked();
119
+ });
120
+ });
121
+
122
+ test.describe('Item', () => {
123
+
124
+ test('should allow me to mark items as complete', async ({ page }) => {
125
+ // create a new todo locator
126
+ const newTodo = page.getByPlaceholder('What needs to be done?');
127
+
128
+ // Create two items.
129
+ for (const item of TODO_ITEMS.slice(0, 2)) {
130
+ await newTodo.fill(item);
131
+ await newTodo.press('Enter');
132
+ }
133
+
134
+ // Check first item.
135
+ const firstTodo = page.getByTestId('todo-item').nth(0);
136
+ await firstTodo.getByRole('checkbox').check();
137
+ await expect(firstTodo).toHaveClass('completed');
138
+
139
+ // Check second item.
140
+ const secondTodo = page.getByTestId('todo-item').nth(1);
141
+ await expect(secondTodo).not.toHaveClass('completed');
142
+ await secondTodo.getByRole('checkbox').check();
143
+
144
+ // Assert completed class.
145
+ await expect(firstTodo).toHaveClass('completed');
146
+ await expect(secondTodo).toHaveClass('completed');
147
+ });
148
+
149
+ test('should allow me to un-mark items as complete', async ({ page }) => {
150
+ // create a new todo locator
151
+ const newTodo = page.getByPlaceholder('What needs to be done?');
152
+
153
+ // Create two items.
154
+ for (const item of TODO_ITEMS.slice(0, 2)) {
155
+ await newTodo.fill(item);
156
+ await newTodo.press('Enter');
157
+ }
158
+
159
+ const firstTodo = page.getByTestId('todo-item').nth(0);
160
+ const secondTodo = page.getByTestId('todo-item').nth(1);
161
+ const firstTodoCheckbox = firstTodo.getByRole('checkbox');
162
+
163
+ await firstTodoCheckbox.check();
164
+ await expect(firstTodo).toHaveClass('completed');
165
+ await expect(secondTodo).not.toHaveClass('completed');
166
+ await checkNumberOfCompletedTodosInLocalStorage(page, 1);
167
+
168
+ await firstTodoCheckbox.uncheck();
169
+ await expect(firstTodo).not.toHaveClass('completed');
170
+ await expect(secondTodo).not.toHaveClass('completed');
171
+ await checkNumberOfCompletedTodosInLocalStorage(page, 0);
172
+ });
173
+
174
+ test('should allow me to edit an item', async ({ page }) => {
175
+ await createDefaultTodos(page);
176
+
177
+ const todoItems = page.getByTestId('todo-item');
178
+ const secondTodo = todoItems.nth(1);
179
+ await secondTodo.dblclick();
180
+ await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]);
181
+ await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
182
+ await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter');
183
+
184
+ // Explicitly assert the new text value.
185
+ await expect(todoItems).toHaveText([
186
+ TODO_ITEMS[0],
187
+ 'buy some sausages',
188
+ TODO_ITEMS[2]
189
+ ]);
190
+ await checkTodosInLocalStorage(page, 'buy some sausages');
191
+ });
192
+ });
193
+
194
+ test.describe('Editing', () => {
195
+ test.beforeEach(async ({ page }) => {
196
+ await createDefaultTodos(page);
197
+ await checkNumberOfTodosInLocalStorage(page, 3);
198
+ });
199
+
200
+ test('should hide other controls when editing', async ({ page }) => {
201
+ const todoItem = page.getByTestId('todo-item').nth(1);
202
+ await todoItem.dblclick();
203
+ await expect(todoItem.getByRole('checkbox')).not.toBeVisible();
204
+ await expect(todoItem.locator('label', {
205
+ hasText: TODO_ITEMS[1],
206
+ })).not.toBeVisible();
207
+ await checkNumberOfTodosInLocalStorage(page, 3);
208
+ });
209
+
210
+ test('should save edits on blur', async ({ page }) => {
211
+ const todoItems = page.getByTestId('todo-item');
212
+ await todoItems.nth(1).dblclick();
213
+ await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
214
+ await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur');
215
+
216
+ await expect(todoItems).toHaveText([
217
+ TODO_ITEMS[0],
218
+ 'buy some sausages',
219
+ TODO_ITEMS[2],
220
+ ]);
221
+ await checkTodosInLocalStorage(page, 'buy some sausages');
222
+ });
223
+
224
+ test('should trim entered text', async ({ page }) => {
225
+ const todoItems = page.getByTestId('todo-item');
226
+ await todoItems.nth(1).dblclick();
227
+ await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages ');
228
+ await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
229
+
230
+ await expect(todoItems).toHaveText([
231
+ TODO_ITEMS[0],
232
+ 'buy some sausages',
233
+ TODO_ITEMS[2],
234
+ ]);
235
+ await checkTodosInLocalStorage(page, 'buy some sausages');
236
+ });
237
+
238
+ test('should remove the item if an empty text string was entered', async ({ page }) => {
239
+ const todoItems = page.getByTestId('todo-item');
240
+ await todoItems.nth(1).dblclick();
241
+ await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('');
242
+ await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
243
+
244
+ await expect(todoItems).toHaveText([
245
+ TODO_ITEMS[0],
246
+ TODO_ITEMS[2],
247
+ ]);
248
+ });
249
+
250
+ test('should cancel edits on escape', async ({ page }) => {
251
+ const todoItems = page.getByTestId('todo-item');
252
+ await todoItems.nth(1).dblclick();
253
+ await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
254
+ await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape');
255
+ await expect(todoItems).toHaveText(TODO_ITEMS);
256
+ });
257
+ });
258
+
259
+ test.describe('Counter', () => {
260
+ test('should display the current number of todo items', async ({ page }) => {
261
+ // create a new todo locator
262
+ const newTodo = page.getByPlaceholder('What needs to be done?');
263
+
264
+ // create a todo count locator
265
+ const todoCount = page.getByTestId('todo-count')
266
+
267
+ await newTodo.fill(TODO_ITEMS[0]);
268
+ await newTodo.press('Enter');
269
+
270
+ await expect(todoCount).toContainText('1');
271
+
272
+ await newTodo.fill(TODO_ITEMS[1]);
273
+ await newTodo.press('Enter');
274
+ await expect(todoCount).toContainText('2');
275
+
276
+ await checkNumberOfTodosInLocalStorage(page, 2);
277
+ });
278
+ });
279
+
280
+ test.describe('Clear completed button', () => {
281
+ test.beforeEach(async ({ page }) => {
282
+ await createDefaultTodos(page);
283
+ });
284
+
285
+ test('should display the correct text', async ({ page }) => {
286
+ await page.locator('.todo-list li .toggle').first().check();
287
+ await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible();
288
+ });
289
+
290
+ test('should remove completed items when clicked', async ({ page }) => {
291
+ const todoItems = page.getByTestId('todo-item');
292
+ await todoItems.nth(1).getByRole('checkbox').check();
293
+ await page.getByRole('button', { name: 'Clear completed' }).click();
294
+ await expect(todoItems).toHaveCount(2);
295
+ await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
296
+ });
297
+
298
+ test('should be hidden when there are no items that are completed', async ({ page }) => {
299
+ await page.locator('.todo-list li .toggle').first().check();
300
+ await page.getByRole('button', { name: 'Clear completed' }).click();
301
+ await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden();
302
+ });
303
+ });
304
+
305
+ test.describe('Persistence', () => {
306
+ test('should persist its data', async ({ page }) => {
307
+ // create a new todo locator
308
+ const newTodo = page.getByPlaceholder('What needs to be done?');
309
+
310
+ for (const item of TODO_ITEMS.slice(0, 2)) {
311
+ await newTodo.fill(item);
312
+ await newTodo.press('Enter');
313
+ }
314
+
315
+ const todoItems = page.getByTestId('todo-item');
316
+ const firstTodoCheck = todoItems.nth(0).getByRole('checkbox');
317
+ await firstTodoCheck.check();
318
+ await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
319
+ await expect(firstTodoCheck).toBeChecked();
320
+ await expect(todoItems).toHaveClass(['completed', '']);
321
+
322
+ // Ensure there is 1 completed item.
323
+ await checkNumberOfCompletedTodosInLocalStorage(page, 1);
324
+
325
+ // Now reload.
326
+ await page.reload();
327
+ await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
328
+ await expect(firstTodoCheck).toBeChecked();
329
+ await expect(todoItems).toHaveClass(['completed', '']);
330
+ });
331
+ });
332
+
333
+ test.describe('Routing', () => {
334
+ test.beforeEach(async ({ page }) => {
335
+ await createDefaultTodos(page);
336
+ // make sure the app had a chance to save updated todos in storage
337
+ // before navigating to a new view, otherwise the items can get lost :(
338
+ // in some frameworks like Durandal
339
+ await checkTodosInLocalStorage(page, TODO_ITEMS[0]);
340
+ });
341
+
342
+ test('should allow me to display active items', async ({ page }) => {
343
+ const todoItem = page.getByTestId('todo-item');
344
+ await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
345
+
346
+ await checkNumberOfCompletedTodosInLocalStorage(page, 1);
347
+ await page.getByRole('link', { name: 'Active' }).click();
348
+ await expect(todoItem).toHaveCount(2);
349
+ await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
350
+ });
351
+
352
+ test('should respect the back button', async ({ page }) => {
353
+ const todoItem = page.getByTestId('todo-item');
354
+ await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
355
+
356
+ await checkNumberOfCompletedTodosInLocalStorage(page, 1);
357
+
358
+ await test.step('Showing all items', async () => {
359
+ await page.getByRole('link', { name: 'All' }).click();
360
+ await expect(todoItem).toHaveCount(3);
361
+ });
362
+
363
+ await test.step('Showing active items', async () => {
364
+ await page.getByRole('link', { name: 'Active' }).click();
365
+ });
366
+
367
+ await test.step('Showing completed items', async () => {
368
+ await page.getByRole('link', { name: 'Completed' }).click();
369
+ });
370
+
371
+ await expect(todoItem).toHaveCount(1);
372
+ await page.goBack();
373
+ await expect(todoItem).toHaveCount(2);
374
+ await page.goBack();
375
+ await expect(todoItem).toHaveCount(3);
376
+ });
377
+
378
+ test('should allow me to display completed items', async ({ page }) => {
379
+ await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
380
+ await checkNumberOfCompletedTodosInLocalStorage(page, 1);
381
+ await page.getByRole('link', { name: 'Completed' }).click();
382
+ await expect(page.getByTestId('todo-item')).toHaveCount(1);
383
+ });
384
+
385
+ test('should allow me to display all items', async ({ page }) => {
386
+ await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
387
+ await checkNumberOfCompletedTodosInLocalStorage(page, 1);
388
+ await page.getByRole('link', { name: 'Active' }).click();
389
+ await page.getByRole('link', { name: 'Completed' }).click();
390
+ await page.getByRole('link', { name: 'All' }).click();
391
+ await expect(page.getByTestId('todo-item')).toHaveCount(3);
392
+ });
393
+
394
+ test('should highlight the currently applied filter', async ({ page }) => {
395
+ await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected');
396
+
397
+ //create locators for active and completed links
398
+ const activeLink = page.getByRole('link', { name: 'Active' });
399
+ const completedLink = page.getByRole('link', { name: 'Completed' });
400
+ await activeLink.click();
401
+
402
+ // Page change - active items.
403
+ await expect(activeLink).toHaveClass('selected');
404
+ await completedLink.click();
405
+
406
+ // Page change - completed items.
407
+ await expect(completedLink).toHaveClass('selected');
408
+ });
409
+ });
410
+
411
+ async function createDefaultTodos(page: Page) {
412
+ // create a new todo locator
413
+ const newTodo = page.getByPlaceholder('What needs to be done?');
414
+
415
+ for (const item of TODO_ITEMS) {
416
+ await newTodo.fill(item);
417
+ await newTodo.press('Enter');
418
+ }
419
+ }
420
+
421
+ async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) {
422
+ return await page.waitForFunction(e => {
423
+ return JSON.parse(localStorage['react-todos']).length === e;
424
+ }, expected);
425
+ }
426
+
427
+ async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) {
428
+ return await page.waitForFunction(e => {
429
+ return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e;
430
+ }, expected);
431
+ }
432
+
433
+ async function checkTodosInLocalStorage(page: Page, title: string) {
434
+ return await page.waitForFunction(t => {
435
+ return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t);
436
+ }, title);
437
+ }
tests/main.spec.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { test, expect } from '@playwright/test';
2
+
3
+ test('has title', async ({ page }) => {
4
+ await page.goto('http://localhost:3000/');
5
+
6
+ // Expect a title "to contain" a substring.
7
+ await expect(page).toHaveTitle(/Clapper/);
8
+ });
9
+
10
+ test('get started link', async ({ page }) => {
11
+ await page.goto('http://localhost:3000/');
12
+
13
+ // TODO: replace by our own real tests
14
+ // Click the get started link.
15
+ // await page.getByRole('link', { name: 'Get started' }).click();
16
+
17
+ // Expects page to have a heading with the name of Installation.
18
+ // await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
19
+ });
tsconfig.json CHANGED
@@ -21,6 +21,6 @@
21
  "@/*": ["./src/*"]
22
  }
23
  },
24
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25
  "exclude": ["node_modules"]
26
  }
 
21
  "@/*": ["./src/*"]
22
  }
23
  },
24
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "vitest.config.mts"],
25
  "exclude": ["node_modules"]
26
  }
vitest.config.mts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vitest/config'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ test: {
7
+ environment: 'jsdom',
8
+ exclude: [
9
+ '**/node_modules/**',
10
+ '**/.next/**',
11
+ '**/tests/**', // <- we ignore since those are Playwright tests, not Vitest tests
12
+ ],
13
+ }
14
+ })