-
Notifications
You must be signed in to change notification settings - Fork 673
feat(first-run-tour): offer templates once the tour ends #14144
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
Changes from 128 commits
Commits
Show all changes
201 commits
Select commit
Hold shift + click to select a range
9897739
feat: add product coachmark onboarding tours
pythongosssss cc74e1d
add assets step to app mode tour
pythongosssss 75af043
- change dialog to use standard component
pythongosssss f80deb9
[automated] Apply ESLint and Oxfmt fixes
actions-user f642384
change dash stroke to solid
pythongosssss 5e23f76
fix: address coachmark tour review feedback
pythongosssss c51f963
trim comments
pythongosssss e9729ca
drop blankCanvas tour
pythongosssss f973626
improve test coverage
pythongosssss 5dbb560
refactor + tidy
pythongosssss 88cd848
fix test
pythongosssss fdfa988
feat: make app-mode tour explicit-only and refine assets step
pythongosssss 5118212
feat: auto-open app-mode tour for populated apps
pythongosssss 07356e3
tidy
pythongosssss b65da23
refactor: slim coachmark target tracking and spotlight chrome
pythongosssss 87d0a11
fix: close the coachmark landing on Escape explicitly
pythongosssss a818b7e
test: open the template browser in the coachmark drift guard
pythongosssss 9b8dd27
refactor: gate coachmark tour listeners behind an active-step component
pythongosssss 5383e23
Merge origin/main into pysssss/product-coachmarks
pythongosssss 3f84d4f
test: cover forced tour start, button labels, and spotlight escape
pythongosssss 64d10da
refactor: place coachmark cards with Floating UI
pythongosssss 831813a
fix: stop coachmark spotlight polling once its target settles
pythongosssss ce8b107
fix: correct coachmark tour skip telemetry and one-shot forcing
pythongosssss 2312b21
refactor: address coachmark tour review feedback
pythongosssss 96c2ae1
feat: add landing image to the app-mode tour
pythongosssss 549200a
fix: address coachmark tour stacking, telemetry, and fixture review n…
pythongosssss e97746f
refactor: address coachmark tour PR feedback
pythongosssss e7cdfc8
chore: ignore derived coachmark keys in the unused-i18n check
pythongosssss 9bcfda8
refactor: reconcile v-coachmark from the element's own data-coach-id
pythongosssss 76abe8e
Merge branch 'main' into pysssss/product-coachmarks
pythongosssss bb39a51
fix: narrow the coachmark i18n exemption to derived step keys
pythongosssss 7630cc9
fix: use the generic Skip label on the coachmark tour landing
pythongosssss 61756e9
fix missing modal backdrop
pythongosssss 9a71b54
refactor: move the coachmark tour state machine into a Pinia store
pythongosssss ccb3cd4
fix: restore the coachmark landing's md max-width
pythongosssss 9730850
fix: keep the coachmark tour alive when outputs clear
pythongosssss 25ce1eb
Merge remote-tracking branch 'origin/main' into pysssss/product-coach…
pythongosssss f1f7c53
feat: rework coachmark assets step and add step-back navigation
pythongosssss 7df585e
flush -> nextTick
pythongosssss 3f5b71a
refactor: use reka FocusScope for coachmark focus trapping
pythongosssss cb1be63
decrease padding
pythongosssss 66b9f32
update wording
pythongosssss dff76db
make button grow, match design
pythongosssss 62806d5
Merge branch 'main' into pysssss/product-coachmarks
pythongosssss 13fd072
Merge branch 'main' into pysssss/product-coachmarks
AustinMroz 62bb4d4
Merge remote-tracking branch 'origin/main' into pysssss/product-coach…
MaanilVerma e9caa31
test(onboarding): fix the coachmark drift-guard's assets-tab selector
MaanilVerma fcd151d
feat(onboarding): accept virtual coachmark targets
MaanilVerma b8c7035
feat(onboarding): add step onEnter hooks and async tour resolvers
MaanilVerma daef85f
feat(onboarding): mask-based scrim for interactive steps
MaanilVerma 44a383e
feat(first-run-tour): resolve canvas nodes as coachmark targets
MaanilVerma 1389a48
refactor(first-run-tour): drop the unused canvas union rect
MaanilVerma 76d612c
refactor(onboarding): spotlight one target, drop the unused extra mas…
MaanilVerma 0f93f63
refactor(first-run-tour): map canvas targets with litegraph's own camera
MaanilVerma 3f20384
refactor(persistence): return a startup outcome instead of routing on…
MaanilVerma 35267ba
feat(first-run-tour): add the onboarding tour feature flag
MaanilVerma 71dd832
fix(tab): merge a caller's class into TabList
MaanilVerma 4a59f74
feat(first-run-tour): decide the first-run entry surface at startup
MaanilVerma a661971
feat(first-run-tour): add the Getting Started screen
MaanilVerma 6e852a7
feat(first-run-tour): resolve template roles from pinned nodes
MaanilVerma 69aa9aa
chore(first-run-tour): drop an unused Getting Started string
MaanilVerma 3143aa9
feat(first-run-tour): build the step sequence from resolved roles
MaanilVerma 7dff28b
test(first-run-tour): clear the teleported screen between tests
MaanilVerma 386812b
test(first-run-tour): guard the role pins against template drift
MaanilVerma edf4363
fix(first-run-tour): centre the Getting Started screen
MaanilVerma db7ad16
test(first-run-tour): fail the drift guard on the templates the grid …
MaanilVerma ed0be4b
feat(onboarding): let higher layers register a tour's steps
MaanilVerma 7b4ca9d
feat(onboarding): guide the card onto a moving canvas target
MaanilVerma bdceede
feat(first-run-tour): build the tour's steps and frame each node
MaanilVerma 312d6ba
feat(first-run-tour): run the tour from a picked template
MaanilVerma b96f82b
test(first-run-tour): cover the tour end to end from Getting Started
MaanilVerma c7a4e2e
fix(first-run-tour): tell a landed run apart from one that never started
MaanilVerma 3c7adef
fix(first-run-tour): spotlight only the step the tour is on
MaanilVerma 587fe99
test(first-run-tour): drive the tour walk to a real run outcome
MaanilVerma 9414aa7
test(first-run-tour): give the step builder the real camera to measur…
MaanilVerma 15cd5e0
feat(first-run-tour): offer templates once the tour ends
MaanilVerma 70fa73c
test(first-run-tour): cover the paywalled run reaching the nudge
MaanilVerma 2b4516e
Merge main
AustinMroz 613ab51
Merge branch 'pysssss/product-coachmarks' into feat/tour-engine-canva…
MaanilVerma e48f50b
Merge branch 'feat/tour-engine-canvas-targets' into feat/getting-star…
MaanilVerma 108d529
Merge branch 'feat/getting-started-screen' into feat/tour-role-resolu…
MaanilVerma 1ef3b25
Merge branch 'feat/tour-role-resolution' into feat/first-run-tour
MaanilVerma 92ae020
Merge branch 'feat/first-run-tour' into feat/first-run-tour-nudge
MaanilVerma d221753
test(first-run-tour): let the drift guard tolerate a lagging backend
MaanilVerma 890f946
Merge branch 'feat/tour-role-resolution' of https://github.com/Comfy-…
MaanilVerma ad4f154
Merge branch 'feat/tour-role-resolution' into feat/first-run-tour
MaanilVerma d3e486f
Merge branch 'feat/first-run-tour' into feat/first-run-tour-nudge
MaanilVerma c671e33
Merge main (again)
AustinMroz 45b1e53
refactor(onboarding): follow the camera instead of polling the target
MaanilVerma 1cfccec
refactor(onboarding): position the card from the target itself
MaanilVerma 7bd2325
Merge branch 'feat/tour-engine-canvas-targets' into feat/getting-star…
MaanilVerma 95c7053
Merge branch 'feat/getting-started-screen' into feat/tour-role-resolu…
MaanilVerma cba63ef
Merge branch 'feat/tour-role-resolution' into feat/first-run-tour
MaanilVerma 45036e0
Merge branch 'feat/first-run-tour' into feat/first-run-tour-nudge
MaanilVerma 7b0893d
refactor(first-run-tour): address review on the Getting Started screen
MaanilVerma dcf4580
Merge branch 'feat/getting-started-screen' into feat/tour-role-resolu…
MaanilVerma a5c4b5d
Merge branch 'feat/tour-role-resolution' into feat/first-run-tour
MaanilVerma bc612a7
Merge branch 'feat/first-run-tour' into feat/first-run-tour-nudge
MaanilVerma 9340261
fix(first-run-tour): finish the tutorial thumbnail rotation
MaanilVerma cb3d4df
Merge branch 'feat/getting-started-screen' into feat/tour-role-resolu…
MaanilVerma bc09396
Merge branch 'feat/tour-role-resolution' into feat/first-run-tour
MaanilVerma 4447ede
Merge branch 'feat/first-run-tour' into feat/first-run-tour-nudge
MaanilVerma 5d353a7
fix(first-run-tour): guard a collapsed viewport and type the tour mocks
MaanilVerma 2328111
Merge branch 'feat/first-run-tour' into feat/first-run-tour-nudge
MaanilVerma 9a5d2be
refactor(onboarding): target canvas nodes by selector, not camera maths
MaanilVerma 87a840f
Merge branch 'feat/tour-engine-canvas-targets' into feat/getting-star…
MaanilVerma eaec0f9
Merge branch 'feat/getting-started-screen' into feat/tour-role-resolu…
MaanilVerma aa404d5
Merge branch 'feat/tour-role-resolution' into feat/first-run-tour
MaanilVerma c17829d
fix(onboarding): arm the card's glide when its target starts moving
MaanilVerma 3dbb3de
Merge branch 'feat/first-run-tour' into feat/first-run-tour-nudge
MaanilVerma 2716aa5
fix(onboarding): address review feedback on the first-run tour
MaanilVerma 99d9842
test(onboarding): pin the card's glide to its motion-safe variant
MaanilVerma 1439645
Merge branch 'feat/first-run-tour' into feat/first-run-tour-nudge
MaanilVerma 0157a13
refactor(onboarding): name the nudge's appearance state for what it is
MaanilVerma 79804df
fix(test): stop the tour walk on Run, not at the step count
MaanilVerma fe4022d
Merge branch 'feat/first-run-tour' into feat/first-run-tour-nudge
MaanilVerma f719108
fix(test): pass the step count into the run helper
MaanilVerma 607d5e9
refactor(onboarding): hold tour progress in one state, not four refs
MaanilVerma 4c2da0c
Merge branch 'feat/tour-engine-canvas-targets' into feat/getting-star…
MaanilVerma b2ad9a8
fix(first-run-tour): mark the tutorial done for users who never see t…
MaanilVerma 55fe4c8
Merge branch 'feat/getting-started-screen' into feat/tour-role-resolu…
MaanilVerma 58cdc15
Merge branch 'feat/tour-role-resolution' into feat/first-run-tour
MaanilVerma 1e83269
fix(onboarding): move the card with the camera, not after it
MaanilVerma 01fbbf2
Merge branch 'feat/first-run-tour' into feat/first-run-tour-nudge
MaanilVerma c504c6a
fix(onboarding): close the first-run tour gaps from the PR-4 review
MaanilVerma 7d35d71
Merge branch 'feat/first-run-tour' into feat/first-run-tour-nudge
MaanilVerma 48358e6
fix(onboarding): end a registered tour when its context goes
MaanilVerma b7464d6
Merge branch 'feat/first-run-tour' into feat/first-run-tour-nudge
MaanilVerma 4defd24
refactor(onboarding): derive canvas coachmark targets from layout, no…
MaanilVerma 69a9893
Merge branch 'feat/tour-engine-canvas-targets' into feat/getting-star…
MaanilVerma eed29ee
Merge branch 'feat/getting-started-screen' into feat/tour-role-resolu…
MaanilVerma b4095ed
Merge branch 'feat/tour-role-resolution' into feat/first-run-tour
MaanilVerma e44a64a
fix(onboarding): close the round-3 gaps in the first-run tour
MaanilVerma 7e2491b
Merge branch 'feat/first-run-tour' into feat/first-run-tour-nudge
MaanilVerma d7379b0
refactor(onboarding): give the tour one transition table
MaanilVerma 7b5610e
Merge branch 'feat/tour-engine-canvas-targets' into feat/getting-star…
MaanilVerma 11a8689
Merge branch 'feat/getting-started-screen' into feat/tour-role-resolu…
MaanilVerma 743088f
Merge branch 'feat/tour-role-resolution' into feat/first-run-tour
MaanilVerma 417999a
refactor(onboarding): one way to end a tour whose context is gone
MaanilVerma ad8a3e6
Merge branch 'feat/first-run-tour' into feat/first-run-tour-nudge
MaanilVerma 44957e8
style(onboarding): group the Loader import with the local ones
MaanilVerma 1047e9c
Merge branch 'feat/tour-role-resolution' into feat/first-run-tour
MaanilVerma c96563b
Merge branch 'feat/getting-started-screen' into feat/tour-role-resolu…
MaanilVerma 7ba1f4a
Merge branch 'feat/first-run-tour' into feat/first-run-tour-nudge
MaanilVerma 2aa059f
fix(onboarding): release the canvas target's observers with the tour
MaanilVerma 54650a1
Merge branch 'feat/tour-engine-canvas-targets' into feat/getting-star…
MaanilVerma dc30081
Merge branch 'feat/tour-role-resolution' into feat/first-run-tour
MaanilVerma 9319617
Merge branch 'feat/getting-started-screen' into feat/tour-role-resolu…
MaanilVerma fde0147
fix(first-run-tour): dispose canvas targets when the tour releases them
MaanilVerma 3aa63f7
fix(first-run-tour): end a run whose socket never came back
MaanilVerma 19c491d
Merge branch 'feat/first-run-tour' into feat/first-run-tour-nudge
MaanilVerma a2d25ee
Merge remote-tracking branch 'origin/main' into feat/tour-engine-canv…
MaanilVerma 733ed53
Merge branch 'feat/tour-engine-canvas-targets' into feat/getting-star…
MaanilVerma 314fbfd
Merge branch 'feat/getting-started-screen' into feat/tour-role-resolu…
MaanilVerma 9e2be58
Merge branch 'feat/tour-role-resolution' into feat/first-run-tour
MaanilVerma 7475c4b
Merge branch 'feat/first-run-tour' into feat/first-run-tour-nudge
MaanilVerma 686ac59
refactor(onboarding): drop comments the code already says
MaanilVerma 2465570
Merge branch 'feat/first-run-tour' into feat/first-run-tour-nudge
MaanilVerma cfde0dd
test(onboarding): pin that changing a step's copy moves nothing else
MaanilVerma 768e8ff
Merge branch 'feat/tour-role-resolution' into feat/first-run-tour
MaanilVerma 7c1f832
Merge branch 'feat/tour-engine-canvas-targets' into feat/getting-star…
MaanilVerma fb2b285
Merge branch 'feat/getting-started-screen' into feat/tour-role-resolu…
MaanilVerma 2fbbd0e
test(onboarding): pin that a step renaming itself moves nothing else
MaanilVerma e6626e4
Merge branch 'feat/first-run-tour' into feat/first-run-tour-nudge
MaanilVerma 2dbe668
fix(onboarding): identify the step attempt, not just the run
MaanilVerma d8cf085
Merge branch 'feat/tour-engine-canvas-targets' into feat/getting-star…
MaanilVerma adc5cf0
Merge branch 'feat/getting-started-screen' into feat/tour-role-resolu…
MaanilVerma 9fad789
Merge branch 'feat/tour-role-resolution' into feat/first-run-tour
MaanilVerma 8be9e73
Merge branch 'feat/first-run-tour' into feat/first-run-tour-nudge
MaanilVerma 7a7a56e
fix: keep onboarding off restored work and out of a reload dead-end
MaanilVerma d5aa5fc
Merge branch 'feat/getting-started-screen' into feat/tour-role-resolu…
MaanilVerma b8273e2
Merge branch 'feat/tour-role-resolution' into feat/first-run-tour
MaanilVerma 27482b7
Merge branch 'feat/first-run-tour' into feat/first-run-tour-nudge
MaanilVerma 3619fdf
fix: drop the startup blank canvas instead of skipping persistence
MaanilVerma 1e0c8e1
Merge branch 'feat/getting-started-screen' into feat/tour-role-resolu…
MaanilVerma 4158b5f
Merge branch 'feat/tour-role-resolution' into feat/first-run-tour
MaanilVerma 7228603
Merge branch 'feat/first-run-tour' into feat/first-run-tour-nudge
MaanilVerma 6fd3f36
test(first-run-tour): close the role-pin coverage gaps
MaanilVerma fd0ba7b
Merge branch 'feat/tour-role-resolution' into feat/first-run-tour
MaanilVerma fd34efc
Merge branch 'feat/first-run-tour' into feat/first-run-tour-nudge
MaanilVerma c722180
fix(first-run-tour): offer fallback templates that actually exist
MaanilVerma 85110b3
Merge branch 'feat/getting-started-screen' into feat/tour-role-resolu…
MaanilVerma 157f6d2
fix(first-run-tour): repin the replaced fallbacks and relax the drift…
MaanilVerma caddfd0
Merge branch 'feat/tour-role-resolution' into feat/first-run-tour
MaanilVerma 8e10410
test(first-run-tour): follow the repinned fallbacks
MaanilVerma e2c4dc2
Merge branch 'feat/first-run-tour' into feat/first-run-tour-nudge
MaanilVerma bac312b
fix(onboarding): fall back through Skip to the card when a step has n…
MaanilVerma 0b96cdf
Merge branch 'feat/tour-engine-canvas-targets' into feat/getting-star…
MaanilVerma 8e6d25a
Merge branch 'feat/getting-started-screen' into feat/tour-role-resolu…
MaanilVerma 73bd3ba
Merge branch 'feat/tour-role-resolution' into feat/first-run-tour
MaanilVerma 2368864
fix(first-run-tour): leave the subscribe button its own paywall dialog
MaanilVerma e197a18
fix(onboarding): count not_started per user and name a crashed resolver
MaanilVerma ba15925
fix(first-run-tour): stop a tour that cannot start rejecting the click
MaanilVerma 7c71e4b
test(onboarding): gate the guards a green suite was not holding
MaanilVerma 5546176
Merge branch 'feat/first-run-tour' into feat/first-run-tour-nudge
MaanilVerma 061045b
fix(telemetry): keep step_count required on the stages that count steps
MaanilVerma 451757a
fix(first-run-tour): stop congratulating a user whose tour never ran
MaanilVerma e56500d
test(first-run-tour): assert the nudge waits out the upgrade dialog
MaanilVerma 13cc1e7
fix(first-run-tour): fail a run the queue never accepted
christian-byrne a75fa92
Merge remote-tracking branch 'origin/feat/first-run-tour' into christ…
christian-byrne 7370588
Merge remote-tracking branch 'origin/main' into christian/rebase-14144
christian-byrne File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| import { expect } from '@playwright/test' | ||
|
|
||
| import type { | ||
| ExportedSubgraph, | ||
| ISerialisedNode, | ||
| SerialisableGraph | ||
| } from '@/lib/litegraph/src/types/serialisation' | ||
| import { CURATED_TEMPLATE_IDS } from '@/renderer/extensions/firstRunTour/gettingStarted/tutorialCards' | ||
| import { TOUR_ROLE_PINS } from '@/renderer/extensions/firstRunTour/roles/tourRolePins' | ||
| import type { | ||
| RolePin, | ||
| RolePins | ||
| } from '@/renderer/extensions/firstRunTour/roles/tourRolePins' | ||
| import { templateApiFixture as test } from '@e2e/fixtures/templateApiFixture' | ||
|
|
||
| const baseUrl = process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188' | ||
|
|
||
| const pinnedTemplates = Object.entries(TOUR_ROLE_PINS) | ||
|
|
||
| function pinnedRoles(pins: RolePins): [string, RolePin][] { | ||
| const roles = { source: pins.source, prompt: pins.prompt, sink: pins.sink } | ||
| return Object.entries(roles).filter( | ||
| (entry): entry is [string, RolePin] => entry[1] !== undefined | ||
| ) | ||
| } | ||
|
|
||
| /** Subgraph definitions nest, and so does the resolver's host-mapping. */ | ||
| function nodesOf( | ||
| graph: SerialisableGraph | ExportedSubgraph | ||
| ): ISerialisedNode[] { | ||
| return [ | ||
| ...(graph.nodes ?? []), | ||
| ...(graph.definitions?.subgraphs ?? []).flatMap(nodesOf) | ||
| ] | ||
| } | ||
|
|
||
| function findNode(workflow: SerialisableGraph, id: number) { | ||
| return nodesOf(workflow).find((node) => String(node.id) === String(id)) | ||
| } | ||
|
|
||
| test.describe('first-run tour role pins', { tag: '@workflow' }, () => { | ||
| test('every pinned node still exists with its pinned type', async ({ | ||
| request | ||
| }) => { | ||
| const unserved: string[] = [] | ||
|
|
||
| for (const [templateId, pins] of pinnedTemplates) { | ||
| const url = new URL(`/templates/${templateId}.json`, baseUrl).toString() | ||
| const response = await request.get(url) | ||
| if (!response.ok()) { | ||
| unserved.push(templateId) | ||
| continue | ||
| } | ||
|
|
||
| const workflow = (await response.json()) as SerialisableGraph | ||
| for (const [role, pin] of pinnedRoles(pins)) { | ||
| expect( | ||
| findNode(workflow, pin.id)?.type, | ||
| `${templateId} pins its ${role} to node ${pin.id}, which this backend no longer serves as a ${pin.type}` | ||
| ).toBe(pin.type) | ||
| } | ||
| } | ||
| if (unserved.length) | ||
| test.info().annotations.push({ | ||
| type: 'unserved templates', | ||
| description: `pins unverified, not served by this backend: ${unserved.join(', ')}` | ||
| }) | ||
| expect( | ||
| pinnedTemplates.length - unserved.length, | ||
| `the Getting Started grid needs ${CURATED_TEMPLATE_IDS.length} templates and this backend serves too few of these pins — unserved: ${unserved.join(', ')}` | ||
| ).toBeGreaterThanOrEqual(CURATED_TEMPLATE_IDS.length) | ||
| }) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,265 @@ | ||
| import { expect, mergeTests } from '@playwright/test' | ||
| import type { Locator, Page } from '@playwright/test' | ||
|
|
||
| import enMessages from '@/locales/en/main.json' with { type: 'json' } | ||
| import { TOUR_ROLE_PINS } from '@/renderer/extensions/firstRunTour/roles/tourRolePins' | ||
| import type { SupportedTemplateId } from '@/renderer/extensions/firstRunTour/roles/tourRolePins' | ||
|
|
||
| import type { PromptResponse } from '@comfyorg/ingest-types' | ||
|
|
||
| import type { AssetResponse } from '@/platform/assets/schemas/assetSchema' | ||
| import type { CloudSubscriptionStatusResponse } from '@/platform/cloud/subscription/composables/useSubscription' | ||
| import type { RemoteConfig } from '@/platform/remoteConfig/types' | ||
|
|
||
| import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage' | ||
| import { ExecutionHelper } from '@e2e/fixtures/helpers/ExecutionHelper' | ||
| import { mockBilling } from '@e2e/fixtures/utils/cloudBillingMocks' | ||
| import { jsonRoute } from '@e2e/fixtures/utils/jsonRoute' | ||
| import { webSocketFixture } from '@e2e/fixtures/ws' | ||
|
|
||
| const wstest = mergeTests(test, webSocketFixture) | ||
|
|
||
| const { firstRun } = enMessages.onboardingCoachmarks | ||
| const GETTING_STARTED_TITLE = enMessages.gettingStarted.title | ||
| const RUN_STEP_TITLE = firstRun.run.title | ||
| const GENERATING_TITLE = firstRun.result.generating.title | ||
| const RESULT_IMAGE_TITLE = firstRun.result.image.title | ||
| const RESULT_FAILED_TITLE = firstRun.result.failed.title | ||
| const CARD_TESTID_PREFIX = 'getting-started-card-' | ||
|
|
||
| /** The prompt id the tour's run is queued under, so WS events can address it. */ | ||
| const TOUR_JOB_ID = 'first-run-tour-prompt' | ||
|
|
||
| /** A prompt the queue accepts, so the walk does not depend on the backend's models. */ | ||
| const QUEUED_PROMPT: PromptResponse = { | ||
| prompt_id: TOUR_JOB_ID, | ||
| number: 1, | ||
| node_errors: {} | ||
| } | ||
|
|
||
| const TOUR_FEATURE_FLAGS: RemoteConfig = { | ||
| onboarding_tour_enabled: true, | ||
| subscription_required: true | ||
| } | ||
|
|
||
| const ACTIVE_SUBSCRIPTION: CloudSubscriptionStatusResponse = { | ||
| is_active: true, | ||
| subscription_id: 'sub_first_run_tour', | ||
| renewal_date: '2099-01-01' | ||
| } | ||
|
|
||
| const NO_ASSETS: AssetResponse = { | ||
| assets: [], | ||
| total: 0, | ||
| has_more: false | ||
| } | ||
|
|
||
| function isPinned(id: string): id is SupportedTemplateId { | ||
| return Object.hasOwn(TOUR_ROLE_PINS, id) | ||
| } | ||
|
|
||
| /** How many steps the card says the tour has, once it says anything. */ | ||
| async function tourLength(card: Locator): Promise<number> { | ||
| await expect(card).toContainText(/Step \d+ of \d+/) | ||
| const label = await card.textContent() | ||
| return Number(/Step \d+ of (\d+)/.exec(label ?? '')?.[1]) | ||
| } | ||
|
|
||
| /** | ||
| * The grid backfills whichever curated templates a backend does not serve, so | ||
| * the walk tours a card that is actually on screen rather than a fixed id. | ||
| */ | ||
| async function firstPinnedTemplateOnScreen( | ||
| page: Page | ||
| ): Promise<SupportedTemplateId> { | ||
| const testIds = await page | ||
| .locator(`[data-testid^="${CARD_TESTID_PREFIX}"]`) | ||
| .evaluateAll((cards) => | ||
| cards.map((card) => card.getAttribute('data-testid') ?? '') | ||
| ) | ||
| const templateId = testIds | ||
| .map((testId) => testId.slice(CARD_TESTID_PREFIX.length)) | ||
| .find(isPinned) | ||
|
|
||
| expect( | ||
| templateId, | ||
| `this backend serves none of the pinned templates: ${testIds.join(', ')}` | ||
| ).toBeDefined() | ||
| return templateId! | ||
| } | ||
|
|
||
| /** | ||
| * The walk the review asked for: a fresh user reaches Getting Started, picks a | ||
| * template, and the tour guides them through to a result. | ||
| */ | ||
| test.describe('First-run tour', { tag: ['@cloud', '@ui'] }, () => { | ||
| test.use({ | ||
| initialSettings: { | ||
| 'Comfy.TutorialCompleted': false, | ||
| 'Comfy.OnboardingCoachmarks.Seen': ['appMode'], | ||
| 'Comfy.VueNodes.Enabled': true | ||
| }, | ||
| initialFeatureFlags: { onboarding_tour_enabled: true } | ||
| }) | ||
|
|
||
| test.beforeEach(async ({ page }) => { | ||
| await page.route('**/api/features', (route) => | ||
| route.fulfill(jsonRoute(TOUR_FEATURE_FLAGS)) | ||
| ) | ||
| await page.route('**/customers/cloud-subscription-status', (route) => | ||
| route.fulfill(jsonRoute(ACTIVE_SUBSCRIPTION)) | ||
| ) | ||
| await page.route('**/api/assets**', (route) => | ||
| route.fulfill(jsonRoute(NO_ASSETS)) | ||
| ) | ||
| }) | ||
|
|
||
| /** | ||
| * The shared half of the walk: a fresh user reaches Getting Started, picks a | ||
| * pinned template, and the tour guides them to Run — where the funded and | ||
| * paywalled branches part. The run is queued under a known id, so whoever | ||
| * goes on to click Run can address it over the websocket. | ||
| */ | ||
| async function tourToRunStep(page: Page) { | ||
| const screen = page.getByRole('dialog', { name: GETTING_STARTED_TITLE }) | ||
| const spotlight = page.getByTestId('coach-spotlight') | ||
| const card = page.getByTestId('coach-card') | ||
|
|
||
| await page.route('**/api/prompt', (route) => | ||
| route.fulfill(jsonRoute(QUEUED_PROMPT)) | ||
| ) | ||
|
|
||
| await expect(screen).toBeVisible() | ||
|
|
||
| const templateId = await firstPinnedTemplateOnScreen(page) | ||
| await page.getByTestId(`${CARD_TESTID_PREFIX}${templateId}`).click() | ||
|
|
||
| await expect(screen).toBeHidden() | ||
| await expect( | ||
| spotlight, | ||
| 'picking a template must spotlight the graph it loaded' | ||
| ).toBeVisible() | ||
|
|
||
| const next = card.getByRole('button', { name: 'Next' }) | ||
| const runTitle = card.getByText(RUN_STEP_TITLE) | ||
| const totalSteps = await tourLength(card) | ||
|
|
||
| for (let step = 1; step < totalSteps; step++) { | ||
| await expect(card).toContainText(`Step ${step} of ${totalSteps}`) | ||
| if (await runTitle.isVisible()) break | ||
| await next.click() | ||
| } | ||
|
|
||
| await expect(runTitle).toBeVisible() | ||
| await expect( | ||
| next, | ||
| 'the Run step must offer no way forward except running' | ||
| ).toBeHidden() | ||
|
|
||
| return { spotlight, card, totalSteps } | ||
| } | ||
|
|
||
| /** Clicks Run and waits for the Result step to admit the run is in flight. */ | ||
| async function runFromTourStep( | ||
| page: Page, | ||
| card: Locator, | ||
| totalSteps: number | ||
| ) { | ||
| await page.getByTestId('queue-button').click() | ||
|
|
||
| await expect( | ||
| card.getByText(GENERATING_TITLE), | ||
| 'the run outlives its step, so the click moves the tour on and Result reports it' | ||
| ).toBeVisible({ timeout: 15_000 }) | ||
| await expect( | ||
| card, | ||
| 'Run hands over to the last step, the one spotlighting where output lands' | ||
| ).toContainText(`Step ${totalSteps} of ${totalSteps}`) | ||
| } | ||
|
|
||
| wstest( | ||
| 'guides a first-run user from a template to a finished result', | ||
| async ({ comfyPage, getWebSocket }) => { | ||
| // A template load, three camera flights and a queued run do not fit the default budget. | ||
| test.slow() | ||
| const { page } = comfyPage | ||
| const execution = new ExecutionHelper(comfyPage, await getWebSocket()) | ||
|
|
||
| const { card, totalSteps } = await tourToRunStep(page) | ||
| await runFromTourStep(page, card, totalSteps) | ||
|
|
||
| execution.executionStart(TOUR_JOB_ID) | ||
| execution.executionSuccess(TOUR_JOB_ID) | ||
|
|
||
| await expect( | ||
| card.getByText(RESULT_IMAGE_TITLE), | ||
| 'the run finished, so the Result step has to stop saying it is still coming' | ||
| ).toBeVisible({ timeout: 15_000 }) | ||
| } | ||
| ) | ||
|
|
||
| wstest( | ||
| 'tells a first-run user when the run produced nothing', | ||
| async ({ comfyPage, getWebSocket }) => { | ||
| test.slow() | ||
| const { page } = comfyPage | ||
| const execution = new ExecutionHelper(comfyPage, await getWebSocket()) | ||
|
|
||
| const { card, totalSteps } = await tourToRunStep(page) | ||
| await runFromTourStep(page, card, totalSteps) | ||
|
|
||
| execution.executionStart(TOUR_JOB_ID) | ||
| execution.executionError(TOUR_JOB_ID, '1', 'the run blew up') | ||
|
|
||
| await expect( | ||
| card.getByText(RESULT_FAILED_TITLE), | ||
| 'announcing a result that does not exist is the bug D2 filed' | ||
| ).toBeVisible({ timeout: 15_000 }) | ||
| } | ||
| ) | ||
|
|
||
| test.describe('without a subscription', () => { | ||
| test.beforeEach(async ({ page }) => { | ||
| await mockBilling(page) | ||
| }) | ||
|
|
||
| test('leaves the nudge until the upgrade dialog closes', async ({ | ||
| comfyPage | ||
| }) => { | ||
| test.slow() | ||
| const { page } = comfyPage | ||
| const { spotlight } = await tourToRunStep(page) | ||
| const nudge = page.getByTestId('first-run-nudge') | ||
| const upgradeDialog = page.getByTestId('dialog-overlay') | ||
|
|
||
| await page.getByTestId('subscribe-to-run-button').click() | ||
|
|
||
| await expect(upgradeDialog).toBeVisible() | ||
| await expect( | ||
| spotlight, | ||
| 'a tour parked on a button that will never run has nowhere to go' | ||
| ).toBeHidden() | ||
|
|
||
| await page.keyboard.press('Escape') | ||
|
|
||
| await expect(upgradeDialog).toBeHidden() | ||
| await expect( | ||
| nudge, | ||
| 'the tour ended, so the user still needs somewhere to go next' | ||
| ).toBeVisible({ timeout: 10_000 }) | ||
| }) | ||
| }) | ||
|
|
||
| test('starts no tour for a user who takes the blank canvas', async ({ | ||
| comfyPage | ||
| }) => { | ||
| const { page } = comfyPage | ||
|
|
||
| await page.getByTestId('getting-started-blank').click() | ||
|
|
||
| await expect( | ||
| page.getByRole('dialog', { name: GETTING_STARTED_TITLE }) | ||
| ).toBeHidden() | ||
| await expect(page.getByTestId('coach-spotlight')).toBeHidden() | ||
| }) | ||
| }) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This never asserts the nudge is hidden while the dialog is open — only that the spotlight is hidden, and that the nudge is visible after Escape. Remove the
dialogStack.length === 0term from the watcher and this test still passes. One line here:await expect(nudge, 'the nudge sits below the modal stack').toBeHidden().