Compare commits

...

9 Commits

Author SHA1 Message Date
445e18642f Feature tree loading indicator is too big and requires scroll to see
Fixes #6477
2025-04-25 15:25:24 -04:00
cc2ae13241 Add missing fail() 2025-04-23 09:23:55 -04:00
980495c94b Merge branch 'main' into pierremtb/issue6413-Back-button-on-SignIn-page-leads-to-Home-page 2025-04-23 09:14:22 -04:00
cdcee56ae2 Merge branch 'main' into pierremtb/issue6413-Back-button-on-SignIn-page-leads-to-Home-page 2025-04-23 08:24:28 -04:00
4067ff4be8 Update e2e/playwright/auth.spec.ts
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-04-23 08:24:16 -04:00
2570713f6d Good bot
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-04-22 14:56:35 -04:00
42cd72c9bb Full sign in e2e test 2025-04-21 14:41:54 -04:00
e53de2952a Add regression test for the bug, and for new cancel button 2025-04-21 11:01:24 -04:00
2a07f5addc Back button on SignIn page leads to Home page
Fixes #6413
2025-04-21 10:05:13 -04:00
13 changed files with 151 additions and 16 deletions

View File

@ -29,11 +29,11 @@ test.describe('Electron app header tests', () => {
test( test(
'User settings has correct shortcut', 'User settings has correct shortcut',
{ tag: '@electron' }, { tag: '@electron' },
async ({ page }, testInfo) => { async ({ page, toolbar }, testInfo) => {
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
// Open the user sidebar menu. // Open the user sidebar menu.
await page.getByTestId('user-sidebar-toggle').click() await toolbar.userSidebarButton.click()
// No space after "User settings" since it's textContent. // No space after "User settings" since it's textContent.
const text = const text =

View File

@ -0,0 +1,53 @@
import { expect, test } from '@e2e/playwright/zoo-test'
// test file is for testing auth functionality
test.describe('Authentication tests', () => {
test(
`The user can sign out and back in`,
{ tag: ['@electron'] },
async ({ page, homePage, signInPage, toolbar, tronApp }) => {
if (!tronApp) {
fail()
}
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.projectSection.waitFor()
await test.step('Click on sign out and expect sign in page', async () => {
await toolbar.userSidebarButton.click()
await toolbar.signOutButton.click()
await expect(signInPage.signInButton).toBeVisible()
})
await test.step('Click on sign in and cancel, click again and expect different code', async () => {
await signInPage.signInButton.click()
await expect(signInPage.userCode).toBeVisible()
const firstUserCode = await signInPage.userCode.textContent()
await signInPage.cancelSignInButton.click()
await expect(signInPage.signInButton).toBeVisible()
await signInPage.signInButton.click()
await expect(signInPage.userCode).toBeVisible()
const secondUserCode = await signInPage.userCode.textContent()
expect(secondUserCode).not.toEqual(firstUserCode)
})
await test.step('Press back button and remain on home page', async () => {
await page.goBack()
await expect(homePage.projectSection).not.toBeVisible()
await expect(signInPage.signInButton).toBeVisible()
})
await test.step('Sign in, activate, and expect home page', async () => {
await signInPage.signInButton.click()
await expect(signInPage.userCode).toBeVisible()
const userCode = await signInPage.userCode.textContent()
expect(userCode).not.toBeNull()
await signInPage.verifyAndConfirmAuth(userCode!)
// Longer timeout than usual here for the wait on home page
await expect(homePage.projectSection).toBeVisible({ timeout: 10000 })
})
}
)
})

View File

@ -18,6 +18,7 @@ import type { Settings } from '@rust/kcl-lib/bindings/Settings'
import { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture' import { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture'
import { EditorFixture } from '@e2e/playwright/fixtures/editorFixture' import { EditorFixture } from '@e2e/playwright/fixtures/editorFixture'
import { HomePageFixture } from '@e2e/playwright/fixtures/homePageFixture' import { HomePageFixture } from '@e2e/playwright/fixtures/homePageFixture'
import { SignInPageFixture } from '@e2e/playwright/fixtures/signInPageFixture'
import { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture' import { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture'
import { ToolbarFixture } from '@e2e/playwright/fixtures/toolbarFixture' import { ToolbarFixture } from '@e2e/playwright/fixtures/toolbarFixture'
@ -66,6 +67,7 @@ export interface Fixtures {
toolbar: ToolbarFixture toolbar: ToolbarFixture
scene: SceneFixture scene: SceneFixture
homePage: HomePageFixture homePage: HomePageFixture
signInPage: SignInPageFixture
} }
export class ElectronZoo { export class ElectronZoo {
@ -387,6 +389,9 @@ const fixturesBasedOnProcessEnvPlatform = {
homePage: async ({ page }: { page: Page }, use: FnUse) => { homePage: async ({ page }: { page: Page }, use: FnUse) => {
await use(new HomePageFixture(page)) await use(new HomePageFixture(page))
}, },
signInPage: async ({ page }: { page: Page }, use: FnUse) => {
await use(new SignInPageFixture(page))
},
} }
if (process.env.PLATFORM === 'web') { if (process.env.PLATFORM === 'web') {

View File

@ -0,0 +1,48 @@
import type { Locator, Page } from '@playwright/test'
import { secrets } from '@e2e/playwright/secrets'
export class SignInPageFixture {
public page: Page
signInButton!: Locator
cancelSignInButton!: Locator
userCode!: Locator
apiBaseUrl!: string
constructor(page: Page) {
this.page = page
this.signInButton = this.page.getByTestId('sign-in-button')
this.cancelSignInButton = this.page.getByTestId('cancel-sign-in-button')
this.userCode = this.page.getByTestId('sign-in-user-code')
// TODO: set this thru env var
this.apiBaseUrl = 'https://api.dev.zoo.dev'
}
async verifyAndConfirmAuth(userCode: string) {
// Device flow: stolen from the tauri days
// https://github.com/KittyCAD/modeling-app/blob/d916c7987452e480719004e6d11fd2e595c7d0eb/e2e/tauri/specs/app.spec.ts#L19
const headers = {
Authorization: `Bearer ${secrets.token}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}
const verifyUrl = `${this.apiBaseUrl}/oauth2/device/verify?user_code=${userCode}`
console.log(`GET ${verifyUrl}`)
const vr = await fetch(verifyUrl, { headers })
console.log(vr.status)
// Device flow: confirm
const confirmUrl = `${this.apiBaseUrl}/oauth2/device/confirm`
const data = JSON.stringify({ user_code: userCode })
console.log(`POST ${confirmUrl} ${data}`)
const cr = await fetch(confirmUrl, {
headers,
method: 'POST',
body: data,
})
console.log(cr.status)
}
}

View File

@ -46,6 +46,9 @@ export class ToolbarFixture {
gizmo!: Locator gizmo!: Locator
gizmoDisabled!: Locator gizmoDisabled!: Locator
loadButton!: Locator loadButton!: Locator
/** User button for the user sidebar menu */
userSidebarButton!: Locator
signOutButton!: Locator
constructor(page: Page) { constructor(page: Page) {
this.page = page this.page = page
@ -82,6 +85,9 @@ export class ToolbarFixture {
// element or two different elements can represent these states. // element or two different elements can represent these states.
this.gizmo = page.getByTestId('gizmo') this.gizmo = page.getByTestId('gizmo')
this.gizmoDisabled = page.getByTestId('gizmo-disabled') this.gizmoDisabled = page.getByTestId('gizmo-disabled')
this.userSidebarButton = page.getByTestId('user-sidebar-toggle')
this.signOutButton = page.getByTestId('user-sidebar-sign-out')
} }
get logoLink() { get logoLink() {

View File

@ -331,6 +331,7 @@ test.describe('Onboarding tests', () => {
test('Avatar text updates depending on image load success', async ({ test('Avatar text updates depending on image load success', async ({
context, context,
page, page,
toolbar,
homePage, homePage,
tronApp, tronApp,
}) => { }) => {
@ -362,7 +363,7 @@ test.describe('Onboarding tests', () => {
await homePage.goToModelingScene() await homePage.goToModelingScene()
// Test that the text in this step is correct // Test that the text in this step is correct
const avatarLocator = page.getByTestId('user-sidebar-toggle').locator('img') const avatarLocator = toolbar.userSidebarButton.locator('img')
const onboardingOverlayLocator = page const onboardingOverlayLocator = page
.getByTestId('onboarding-content') .getByTestId('onboarding-content')
.locator('div') .locator('div')
@ -404,6 +405,7 @@ test.describe('Onboarding tests', () => {
test("Avatar text doesn't mention avatar when no avatar", async ({ test("Avatar text doesn't mention avatar when no avatar", async ({
context, context,
page, page,
toolbar,
homePage, homePage,
tronApp, tronApp,
}) => { }) => {
@ -435,7 +437,7 @@ test.describe('Onboarding tests', () => {
await homePage.goToModelingScene() await homePage.goToModelingScene()
// Test that the text in this step is correct // Test that the text in this step is correct
const sidebar = page.getByTestId('user-sidebar-toggle') const sidebar = toolbar.userSidebarButton
const avatar = sidebar.locator('img') const avatar = sidebar.locator('img')
const onboardingOverlayLocator = page const onboardingOverlayLocator = page
.getByTestId('onboarding-content') .getByTestId('onboarding-content')
@ -464,6 +466,7 @@ test.describe('Onboarding tests', () => {
test('Restarting onboarding on desktop takes one attempt', async ({ test('Restarting onboarding on desktop takes one attempt', async ({
context, context,
page, page,
toolbar,
tronApp, tronApp,
}) => { }) => {
test.fixme(orRunWhenFullSuiteEnabled()) test.fixme(orRunWhenFullSuiteEnabled())
@ -502,7 +505,7 @@ test('Restarting onboarding on desktop takes one attempt', async ({
.filter({ hasText: 'Tutorial Project 00' }) .filter({ hasText: 'Tutorial Project 00' })
const tutorialModalText = page.getByText('Welcome to Design Studio!') const tutorialModalText = page.getByText('Welcome to Design Studio!')
const tutorialDismissButton = page.getByRole('button', { name: 'Dismiss' }) const tutorialDismissButton = page.getByRole('button', { name: 'Dismiss' })
const userMenuButton = page.getByTestId('user-sidebar-toggle') const userMenuButton = toolbar.userSidebarButton
const userMenuSettingsButton = page.getByRole('button', { const userMenuSettingsButton = page.getByRole('button', {
name: 'User settings', name: 'User settings',
}) })

View File

@ -1962,13 +1962,13 @@ test(
test( test(
'Settings persist across restarts', 'Settings persist across restarts',
{ tag: '@electron' }, { tag: '@electron' },
async ({ page, scene, cmdBar }, testInfo) => { async ({ page, toolbar }, testInfo) => {
await test.step('We can change a user setting like theme', async () => { await test.step('We can change a user setting like theme', async () => {
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
await page.getByTestId('user-sidebar-toggle').click() await toolbar.userSidebarButton.click()
await page.getByTestId('user-settings').click() await page.getByTestId('user-settings').click()
@ -1995,7 +1995,7 @@ test(
test( test(
'Original project name persist after onboarding', 'Original project name persist after onboarding',
{ tag: '@electron' }, { tag: '@electron' },
async ({ page }, testInfo) => { async ({ page, toolbar }, testInfo) => {
test.fixme(orRunWhenFullSuiteEnabled()) test.fixme(orRunWhenFullSuiteEnabled())
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
@ -2007,7 +2007,7 @@ test(
}) })
await test.step('Should go through onboarding', async () => { await test.step('Should go through onboarding', async () => {
await page.getByTestId('user-sidebar-toggle').click() await toolbar.userSidebarButton.click()
await page.getByTestId('user-settings').click() await page.getByTestId('user-settings').click()
await page.getByRole('button', { name: 'Replay Onboarding' }).click() await page.getByRole('button', { name: 'Replay Onboarding' }).click()

View File

@ -7,7 +7,7 @@ export const Auth = ({ children }: React.PropsWithChildren) => {
const isLoggingIn = authState.matches('checkIfLoggedIn') const isLoggingIn = authState.matches('checkIfLoggedIn')
return isLoggingIn ? ( return isLoggingIn ? (
<Loading> <Loading className="h-screen">
<span data-testid="initial-load">Loading Design Studio...</span> <span data-testid="initial-load">Loading Design Studio...</span>
</Loading> </Loading>
) : ( ) : (

View File

@ -430,7 +430,7 @@ export const EngineStream = (props: {
{![EngineStreamState.Playing, EngineStreamState.Paused].some( {![EngineStreamState.Playing, EngineStreamState.Paused].some(
(s) => s === engineStreamState.value (s) => s === engineStreamState.value
) && ( ) && (
<Loading dataTestId="loading-engine" className="fixed inset-0"> <Loading dataTestId="loading-engine" className="fixed inset-0 h-screen">
Connecting to engine Connecting to engine
</Loading> </Loading>
)} )}

View File

@ -111,7 +111,7 @@ const Loading = ({ children, className, dataTestId }: LoadingProps) => {
return ( return (
<div <div
className={`body-bg flex flex-col items-center justify-center h-screen ${colorClass} ${className}`} className={`body-bg flex flex-col items-center justify-center ${colorClass} ${className}`}
data-testid={dataTestId ? dataTestId : 'loading'} data-testid={dataTestId ? dataTestId : 'loading'}
> >
{isUnrecoverableError ? ( {isUnrecoverableError ? (

View File

@ -15,6 +15,7 @@ export function useAuthNavigation() {
// Subscribe to the auth state of the app and navigate accordingly. // Subscribe to the auth state of the app and navigate accordingly.
useEffect(() => { useEffect(() => {
console.log('authState', authState.value)
if ( if (
authState.matches('loggedIn') && authState.matches('loggedIn') &&
location.pathname.includes(PATHS.SIGN_IN) location.pathname.includes(PATHS.SIGN_IN)
@ -26,5 +27,5 @@ export function useAuthNavigation() {
) { ) {
navigate(PATHS.SIGN_IN) navigate(PATHS.SIGN_IN)
} }
}, [authState]) }, [authState, location.pathname])
} }

View File

@ -346,7 +346,7 @@ const Home = () => {
className="flex-1 overflow-y-auto pr-2 pb-24" className="flex-1 overflow-y-auto pr-2 pb-24"
> >
{state?.matches('Reading projects') ? ( {state?.matches('Reading projects') ? (
<Loading>Loading your Projects...</Loading> <Loading className="h-screen">Loading your Projects...</Loading>
) : ( ) : (
<> <>
{searchResults.length > 0 ? ( {searchResults.length > 0 ? (

View File

@ -76,6 +76,11 @@ const SignIn = () => {
authActor.send({ type: 'Log in', token }) authActor.send({ type: 'Log in', token })
} }
const cancelSignIn = async () => {
authActor.send({ type: 'Log out' })
setUserCode('')
}
return ( return (
<main <main
className="bg-primary h-screen grid place-items-stretch m-0 p-2" className="bg-primary h-screen grid place-items-stretch m-0 p-2"
@ -128,7 +133,10 @@ const SignIn = () => {
<p className="text-xs"> <p className="text-xs">
You should see the following code in your browser You should see the following code in your browser
</p> </p>
<p className="text-lg font-bold inline-flex gap-1"> <p
className="text-lg font-bold inline-flex gap-1"
data-testid="sign-in-user-code"
>
{userCode.split('').map((char, i) => ( {userCode.split('').map((char, i) => (
<span <span
key={i} key={i}
@ -141,6 +149,17 @@ const SignIn = () => {
</span> </span>
))} ))}
</p> </p>
<button
onClick={toSync(cancelSignIn, reportRejection)}
className={
'm-0 mt-8 w-fit flex gap-4 items-center px-3 py-1 ' +
'!border-transparent !text-lg !text-chalkboard-10 !bg-primary hover:hue-rotate-15'
}
data-testid="cancel-sign-in-button"
>
<CustomIcon name="arrowLeft" className="w-6 h-6" />
Cancel
</button>
</> </>
)} )}
</div> </div>
@ -187,7 +206,7 @@ const SignIn = () => {
'm-0 mt-8 flex gap-4 items-center px-3 py-1 ' + 'm-0 mt-8 flex gap-4 items-center px-3 py-1 ' +
'!border-transparent !text-lg !text-chalkboard-10 !bg-primary hover:hue-rotate-15' '!border-transparent !text-lg !text-chalkboard-10 !bg-primary hover:hue-rotate-15'
} }
data-testid="sign-in-button" data-testid="view-sample-button"
> >
View this sample View this sample
<CustomIcon name="arrowRight" className="w-6 h-6" /> <CustomIcon name="arrowRight" className="w-6 h-6" />