import { test, expect } from '@playwright/test' import fsp from 'fs/promises' import { getUtils, setup, setupElectron, tearDown } from './test-utils' import { bracket } from 'lib/exampleKcl' import { onboardingPaths } from 'routes/Onboarding/paths' import { TEST_SETTINGS_KEY, TEST_SETTINGS_ONBOARDING_START, TEST_SETTINGS_ONBOARDING_EXPORT, TEST_SETTINGS_ONBOARDING_PARAMETRIC_MODELING, TEST_SETTINGS_ONBOARDING_USER_MENU, } from './storageStates' import * as TOML from '@iarna/toml' test.beforeEach(async ({ context, page }, testInfo) => { if (testInfo.tags.includes('@electron')) { return } await setup(context, page) }) test.afterEach(async ({ page }, testInfo) => { await tearDown(page, testInfo) }) test.describe('Onboarding tests', () => { test('Onboarding code is shown in the editor', async ({ page }) => { const u = await getUtils(page) // Override beforeEach test setup await page.addInitScript( async ({ settingsKey }) => { // Give no initial code, so that the onboarding start is shown immediately localStorage.removeItem('persistCode') localStorage.removeItem(settingsKey) }, { settingsKey: TEST_SETTINGS_KEY } ) await page.setViewportSize({ width: 1200, height: 500 }) await u.waitForAuthSkipAppStart() // Test that the onboarding pane loaded await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible() // *and* that the code is shown in the editor await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket') }) test('Code resets after confirmation', async ({ page }) => { const initialCode = `const sketch001 = startSketchOn('XZ')` // Load the page up with some code so we see the confirmation warning // when we go to replay onboarding await page.addInitScript((code) => { localStorage.setItem('persistCode', code) }, initialCode) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) await u.waitForAuthSkipAppStart() // Replay the onboarding await page.getByRole('link', { name: 'Settings' }).last().click() const replayButton = page.getByRole('button', { name: 'Replay onboarding' }) await expect(replayButton).toBeVisible() await replayButton.click() // Ensure we see the warning, and that the code has not yet updated await expect( page.getByText('Replaying onboarding resets your code') ).toBeVisible() await expect(page.locator('.cm-content')).toHaveText(initialCode) const nextButton = page.getByTestId('onboarding-next') await expect(nextButton).toBeVisible() await nextButton.click() // Ensure we see the introduction and that the code has been reset await expect(page.getByText('Welcome to Modeling App!')).toBeVisible() await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket') // Ensure we persisted the code to local storage. // Playwright's addInitScript method unfortunately will reset // this code if we try reloading the page as a test, // so this is our best way to test persistence afaik. expect( await page.evaluate(() => { return localStorage.getItem('persistCode') }) ).toContain('// Shelf Bracket') }) test('Click through each onboarding step', async ({ page }) => { const u = await getUtils(page) // Override beforeEach test setup await page.addInitScript( async ({ settingsKey, settings }) => { // Give no initial code, so that the onboarding start is shown immediately localStorage.setItem('persistCode', '') localStorage.setItem(settingsKey, settings) }, { settingsKey: TEST_SETTINGS_KEY, settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING_START }), } ) await page.setViewportSize({ width: 1200, height: 1080 }) await u.waitForAuthSkipAppStart() // Test that the onboarding pane loaded await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible() const nextButton = page.getByTestId('onboarding-next') while ((await nextButton.innerText()) !== 'Finish') { await expect(nextButton).toBeVisible() await nextButton.click() } // Finish the onboarding await expect(nextButton).toBeVisible() await nextButton.click() // Test that the onboarding pane is gone await expect(page.getByTestId('onboarding-content')).not.toBeVisible() await expect(page.url()).not.toContain('onboarding') }) test('Onboarding redirects and code updating', async ({ page }) => { const u = await getUtils(page) // Override beforeEach test setup await page.addInitScript( async ({ settingsKey, settings }) => { // Give some initial code, so we can test that it's cleared localStorage.setItem('persistCode', 'const sigmaAllow = 15000') localStorage.setItem(settingsKey, settings) }, { settingsKey: TEST_SETTINGS_KEY, settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING_EXPORT }), } ) await page.setViewportSize({ width: 1200, height: 500 }) await u.waitForAuthSkipAppStart() // Test that the redirect happened await expect(page.url().split(':3000').slice(-1)[0]).toBe( `/file/%2Fbrowser%2Fmain.kcl/onboarding/export` ) // Test that you come back to this page when you refresh await page.reload() await expect(page.url().split(':3000').slice(-1)[0]).toBe( `/file/%2Fbrowser%2Fmain.kcl/onboarding/export` ) // Test that the onboarding pane loaded const title = page.locator('[data-testid="onboarding-content"]') await expect(title).toBeAttached() // Test that the code changes when you advance to the next step await page.locator('[data-testid="onboarding-next"]').click() await expect(page.locator('.cm-content')).toHaveText('') // Test that the code is not empty when you click on the next step await page.locator('[data-testid="onboarding-next"]').click() await expect(page.locator('.cm-content')).toHaveText(/.+/) }) test('Onboarding code gets reset to demo on Interactive Numbers step', async ({ page, }) => { test.skip( process.platform === 'darwin', "Skip on macOS, because Playwright isn't behaving the same as the actual browser" ) const u = await getUtils(page) const badCode = `// This is bad code we shouldn't see` // Override beforeEach test setup await page.addInitScript( async ({ settingsKey, settings, badCode }) => { localStorage.setItem('persistCode', badCode) localStorage.setItem(settingsKey, settings) }, { settingsKey: TEST_SETTINGS_KEY, settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING_PARAMETRIC_MODELING, }), badCode, } ) await page.setViewportSize({ width: 1200, height: 1080 }) await u.waitForAuthSkipAppStart() await page.waitForURL('**' + onboardingPaths.PARAMETRIC_MODELING, { waitUntil: 'domcontentloaded', }) const bracketNoNewLines = bracket.replace(/\n/g, '') // Check the code got reset on load await expect(page.locator('#code-pane')).toBeVisible() await expect(u.codeLocator).toHaveText(bracketNoNewLines, { timeout: 10_000, }) // Mess with the code again await u.codeLocator.selectText() await u.codeLocator.fill(badCode) await expect(u.codeLocator).toHaveText(badCode) // Click to the next step await page.locator('[data-testid="onboarding-next"]').click() await page.waitForURL('**' + onboardingPaths.INTERACTIVE_NUMBERS, { waitUntil: 'domcontentloaded', }) // Check that the code has been reset await expect(u.codeLocator).toHaveText(bracketNoNewLines) }) test('Avatar text updates depending on image load success', async ({ page, }) => { // Override beforeEach test setup await page.addInitScript( async ({ settingsKey, settings }) => { localStorage.setItem(settingsKey, settings) }, { settingsKey: TEST_SETTINGS_KEY, settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING_USER_MENU, }), } ) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) await u.waitForAuthSkipAppStart() await page.waitForURL('**/file/**', { waitUntil: 'domcontentloaded' }) // Test that the text in this step is correct const avatarLocator = await page .getByTestId('user-sidebar-toggle') .locator('img') const onboardingOverlayLocator = await page .getByTestId('onboarding-content') .locator('div') .nth(1) // Expect the avatar to be visible and for the text to reference it await expect(avatarLocator).toBeVisible() await expect(onboardingOverlayLocator).toBeVisible() await expect(onboardingOverlayLocator).toContainText('your avatar') // This is to force the avatar to 404. // For our test image (only triggers locally. on CI, it's Kurt's / // gravatar image ) await page.route('/cat.jpg', async (route) => { await route.fulfill({ status: 404, contentType: 'text/plain', body: 'Not Found!', }) }) // 404 the CI avatar image await page.route('https://lh3.googleusercontent.com/**', async (route) => { await route.fulfill({ status: 404, contentType: 'text/plain', body: 'Not Found!', }) }) await page.reload({ waitUntil: 'domcontentloaded' }) // Now expect the text to be different await expect(avatarLocator).not.toBeVisible() await expect(onboardingOverlayLocator).toBeVisible() await expect(onboardingOverlayLocator).toContainText('the menu button') }) test("Avatar text doesn't mention avatar when no avatar", async ({ page, }) => { // Override beforeEach test setup await page.addInitScript( async ({ settingsKey, settings }) => { localStorage.setItem(settingsKey, settings) localStorage.setItem('FORCE_NO_IMAGE', 'FORCE_NO_IMAGE') }, { settingsKey: TEST_SETTINGS_KEY, settings: TOML.stringify({ settings: TEST_SETTINGS_ONBOARDING_USER_MENU, }), } ) const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) await u.waitForAuthSkipAppStart() await page.waitForURL('**/file/**', { waitUntil: 'domcontentloaded' }) // Test that the text in this step is correct const sidebar = page.getByTestId('user-sidebar-toggle') const avatar = sidebar.locator('img') const onboardingOverlayLocator = page .getByTestId('onboarding-content') .locator('div') .nth(1) // Expect the avatar to be visible and for the text to reference it await expect(avatar).not.toBeVisible() await expect(onboardingOverlayLocator).toBeVisible() await expect(onboardingOverlayLocator).toContainText('the menu button') // Test we mention what else is in this menu for https://github.com/KittyCAD/modeling-app/issues/2939 // which doesn't deserver its own full test spun up const userMenuFeatures = [ 'manage your account', 'report a bug', 'request a feature', 'sign out', ] for (const feature of userMenuFeatures) { await expect(onboardingOverlayLocator).toContainText(feature) } }) }) test( 'Restarting onboarding on desktop takes one attempt', { tag: '@electron' }, async ({ browser: _ }, testInfo) => { test.skip( process.platform === 'win32', 'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557' ) const { electronApp, page } = await setupElectron({ testInfo, folderSetupFn: async (dir) => { await fsp.mkdir(`${dir}/router-template-slate`, { recursive: true }) await fsp.copyFile( 'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl', `${dir}/router-template-slate/main.kcl` ) }, }) // Our constants const u = await getUtils(page) const projectCard = page.getByText('router-template-slate') const helpMenuButton = page.getByRole('button', { name: 'Help and resources', }) const restartOnboardingButton = page.getByRole('button', { name: 'Reset onboarding', }) const restartConfirmationButton = page.getByRole('button', { name: 'Make a new project', }) const tutorialProjectIndicator = page.getByText('Tutorial Project 00') const tutorialModalText = page.getByText('Welcome to Modeling App!') const tutorialDismissButton = page.getByRole('button', { name: 'Dismiss' }) const userMenuButton = page.getByTestId('user-sidebar-toggle') const userMenuSettingsButton = page.getByRole('button', { name: 'User settings', }) const settingsHeading = page.getByRole('heading', { name: 'Settings', exact: true, }) const restartOnboardingSettingsButton = page.getByRole('button', { name: 'Replay onboarding', }) await test.step('Navigate into project', async () => { await page.setViewportSize({ width: 1200, height: 500 }) page.on('console', console.log) await expect( page.getByRole('heading', { name: 'Your Projects' }) ).toBeVisible() await expect(projectCard).toBeVisible() await projectCard.click() await u.waitForPageLoad() }) await test.step('Restart the onboarding from help menu', async () => { await helpMenuButton.click() await restartOnboardingButton.click() await expect(restartConfirmationButton).toBeVisible() await restartConfirmationButton.click() }) await test.step('Confirm that the onboarding has restarted', async () => { await expect(tutorialProjectIndicator).toBeVisible() await expect(tutorialModalText).toBeVisible() await tutorialDismissButton.click() }) await test.step('Clear code and restart onboarding from settings', async () => { await u.openKclCodePanel() await expect(u.codeLocator).toContainText('// Shelf Bracket') await u.codeLocator.selectText() await u.codeLocator.fill('') await test.step('Navigate to settings', async () => { await userMenuButton.click() await userMenuSettingsButton.click() await expect(settingsHeading).toBeVisible() await expect(restartOnboardingSettingsButton).toBeVisible() }) await restartOnboardingSettingsButton.click() // Since the code is empty, we should not see the confirmation dialog await expect(restartConfirmationButton).not.toBeVisible() await expect(tutorialProjectIndicator).toBeVisible() await expect(tutorialModalText).toBeVisible() }) await electronApp.close() } )