-
Notifications
You must be signed in to change notification settings - Fork 0
docs(onboarding) - add e2e happy path #1010
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
Open
jordividaller
wants to merge
27
commits into
main
Choose a base branch
from
test/pbyr-3802-e2e-oboarding-scenario
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
9837e96
Setup first test for e2e first page basic standard onboarding flow
e7d854a
reorganize e2e fill form helpers + add first step
abe45da
complete basic onboarding e2e test
eebe27f
fix format errors
c988b84
Add Spain scenario (and keep the first one in case of we need it)
2b2e9a0
Wait for loading before testing next page
b937c13
back to only one test for basic employee
e7b5b58
Format code
656b83b
reorganize order of filling fields
0576f5f
new email to avoid force France
f12b848
remove unusued import
344cd5b
Fix country selection bug with webkit and use specific company for th…
0b15130
fix bug with hidden button in the viewport
a819502
Fix bug when selecting country
e6bfe7a
Format
2996060
Fix CI e2e
b59c327
Refactoring way to fill forms
f633d47
Re-order forn
8b660f7
add claude.md
608b751
Improve locator robustness
dbf6701
Add logs to see if JSON Schema is the same in CI
624c52c
Debug to understand if JSON Schema is the same
7bc4f62
Remove log and overtime_compensation_method field
b28c703
remove ^= and fill experience_Level with all the text value
e8e77c0
check if feature flag works
2ddbaaf
add suggestion
gabrielseco ce8244e
remove cssID
gabrielseco 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| # CLAUDE.md | ||
|
|
||
| This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. | ||
|
|
||
| ## What this is | ||
|
|
||
| `@remoteoss/remote-flows` is a React component library (npm package) that exposes embeddable HR/employment flows (Cost Calculator, Onboarding, Contractor Onboarding, Termination, Contract Amendment, Create Company) for Remote's platform. It is consumed by external partners, so the public API in [src/index.tsx](src/index.tsx) is contract-bound: prefer additive changes, and treat type/prop/hook signature changes as breaking. | ||
|
|
||
| ## Commands | ||
|
|
||
| ```bash | ||
| npm run dev # tsup watch build (link this package into example/ for live dev) | ||
| npm run build # production bundle (tsup) | ||
| npm test # vitest (jsdom). Single test: npx vitest run path/to/file.test.tsx | ||
| npm run test:coverage # coverage with thresholds from scripts/coverage-utils | ||
| npm run type-check # tsc --noEmit | ||
| npm run lint # oxlint | ||
| npm run format # oxfmt | ||
| npm run check-format # oxfmt --check (CI gate) | ||
| npm run check-exports # @arethetypeswrong/cli — verifies the package.json exports map | ||
| npm run size # bundle analysis → out/bundle-analysis.json | ||
| npm run size:check # fails if .sizelimit.json thresholds are exceeded | ||
| npm run openapi-ts # regenerate src/client/* from production gateway OpenAPI | ||
| npm run openapi-ts:local # regenerate from local gateway (openapi-ts.config.local.ts) | ||
| npm run ci # full local CI: build + check-format + check-exports + lint + type-check + test | ||
| ``` | ||
|
|
||
| The `example/` app is a separate workspace (its own `package.json`, Vite + Express dev server on `:3001`). To work against local changes: `npm link` in repo root, then `npm link @remoteoss/remote-flows` inside `example/`, then run `npm run dev` in both. E2E lives in [example/e2e/](example/e2e/) and is run with `npm run test:e2e` from `example/` (Playwright). E2E is excluded from the root vitest run. | ||
|
|
||
| ## Architecture | ||
|
|
||
| ### Provider chain | ||
|
|
||
| [src/RemoteFlowsProvider.tsx](src/RemoteFlowsProvider.tsx) is the single root every consumer wraps their flows in. It composes, outside-in: `ErrorContextProvider` → `RemoteFlowsErrorBoundary` → `QueryClientProvider` (shared `queryClient` from [src/queryConfig.ts](src/queryConfig.ts)) → `FormFieldsProvider` (merges user-provided field components with `lazyDefaultComponents`) → `RemoteFlowContext` (carries the API `Client`) → `ThemeProvider`. The API client is created once via `useRef(createClient(...))` — `auth`, `proxy`, `environment`, `credentials` are read at construction time only. | ||
|
|
||
| ### Flow anatomy | ||
|
|
||
| Each flow under [src/flows/](src/flows/) is self-contained and must not import from sibling flows. Standard layout: | ||
|
|
||
| ``` | ||
| flows/<FlowName>/ | ||
| index.ts # public exports (re-exported from src/index.tsx) | ||
| <FlowName>Flow.tsx # render-prop container component | ||
| <FlowName>Form.tsx # form component | ||
| <FlowName>SubmitButton.tsx / ResetButton.tsx # must be used inside the flow's render prop | ||
| hooks.tsx # headless use<FlowName>() — exposes the same bag the render prop receives | ||
| api.ts # React Query hooks / queryOptions for this flow | ||
| context.ts # flow-local context (if needed) | ||
| types.ts | ||
| components/ # step components (BasicInformationStep, ContractDetailsStep, …) | ||
| json-schemas/ # static schema fallbacks | ||
| tests/ # fixtures.ts + *.test.tsx | ||
| utils.ts | ||
| ``` | ||
|
|
||
| Flows are exposed two ways: the prebuilt `<FlowNameFlow render={...}>` component, **and** the `useFlowName()` headless hook for fully custom UIs. Both surfaces must stay in sync. | ||
|
|
||
| ### Multi-step state | ||
|
|
||
| [src/flows/useStepState.ts](src/flows/useStepState.ts) is the shared step-state primitive used across multi-step flows (Onboarding, ContractorOnboarding, CreateCompany, Termination). It tracks `currentStep`, transitions, and per-step values. Consumers switch on `flowBag.stepState.currentStep.name`. | ||
|
|
||
| ### Generated API client | ||
|
|
||
| `src/client/` is **fully generated** by `@hey-api/openapi-ts` from [openapi-ts.config.ts](openapi-ts.config.ts) (production gateway) or `openapi-ts.config.local.ts` (local gateway). Never hand-edit `*.gen.ts`. After schema changes, run `npm run openapi-ts` and commit the regenerated files. Import types from `src/client/types.gen.ts` and SDK functions from `src/client/sdk.gen.ts`. `.oxlintrc.json` ignores `src/client/**/*.gen.ts` from lint. | ||
|
|
||
| ### Forms | ||
|
|
||
| Forms use **React Hook Form + Yup + `@remoteoss/remote-json-schema-form-kit`**. Field rendering is delegated to the `FormFieldsContext` component map; consumers override per-type renderers via the `<RemoteFlows components={...}>` prop, and built-ins come from [src/lazy-default-components.ts](src/lazy-default-components.ts) (lazy-loaded to keep the bundle small). The `flowBag` exposes `handleValidation` and `parseFormValues` — both are **async** (changed in v1.0.0, see [MIGRATION.md](MIGRATION.md)). | ||
|
|
||
| ### React Query patterns | ||
|
|
||
| Two patterns coexist; pick deliberately: | ||
|
|
||
| - **`queryOptions` factory** (preferred when transformations vary per consumer): export `xxxOptions(client, ...)` returning `queryOptions({...})`. Consumers spread it into `useQuery`/`useSuspenseQuery`/`useQueries` and add their own `select`/`enabled`/`staleTime`. Reference: [src/common/api/countries.ts](src/common/api/countries.ts). | ||
| - **Custom hook** (preferred when the transformation is fixed and idempotent for every consumer): wrap `useQuery` directly. Reference: [src/common/api/identity.ts](src/common/api/identity.ts). | ||
|
|
||
| Detailed guidance lives in [.cursor/rules/react-query-abstractions.mdc](.cursor/rules/react-query-abstractions.mdc). | ||
|
|
||
| ### Mutation error handling | ||
|
|
||
| All mutations go through `mutationToPromise()` and `mutateAsyncOrThrow` (the plain `mutateAsync` path is deprecated). When catching errors, **always** narrow with `isMutationError(error)` from [src/lib/mutations.ts](src/lib/mutations.ts) before accessing `normalizedErrors`, `fieldErrors`, `rawError`, `response`. Re-throw after handling. | ||
|
|
||
| ### Theming & CSS | ||
|
|
||
| Tailwind v4 (`@tailwindcss/postcss`) + CSS variables. Public component classes are prefixed `RemoteFlows__` (e.g. `RemoteFlows__Button`, `RemoteFlows__CostCalculatorForm`); variants use `RemoteFlows_VariantName`. Theme tokens (`colors`, `spacing`, `borderRadius`, `font`) are injected as CSS custom properties by `applyTheme()` in [src/lib/applyTheme.ts](src/lib/applyTheme.ts). Class merging uses the `cn()` helper. | ||
|
|
||
| ### Package exports & bundle | ||
|
|
||
| [package.json](package.json) ships **multiple ESM entry points** — `.`, `./internals`, `./default-components`, `./flows/*`, `./styles`, `./styles.css`, `./index.css`. Tsup builds each as a separate chunk ([tsup.config.ts](tsup.config.ts)), `react`/`react-dom` are externals, `react-hook-form` + `@hookform/resolvers` are bundled (`noExternal`). Bundle limits in [.sizelimit.json](.sizelimit.json) are enforced in CI by `.github/workflows/size-check.yml`. The `internals` entry point has **no semver guarantees**. | ||
|
|
||
| ## Conventions worth knowing | ||
|
|
||
| - **Vitest globals are enabled** (`vitest.config.ts`). Do not import `describe`/`it`/`expect`/`vi`/`beforeEach` from `vitest` — use them as globals. | ||
| - **Strict equality in mocks** — use `toHaveBeenCalledWith({ exact: 'shape' })`, not `expect.objectContaining()`. | ||
| - **MSW for API mocking** in tests; handlers go via `src/tests/server.ts`, fixtures live in each flow's `tests/fixtures.ts`. | ||
| - **Tests must wrap React Query consumers** in a `QueryClientProvider` and `queryClient.clear()` between tests. | ||
| - **`no-console: warn`** in oxlint — only `console.warn` / `console.error` permitted. | ||
| - **No `any`** — `typescript/no-explicit-any: error`. Use the `$TSFixMe` type from `src/types/remoteFlows.ts` for genuine workarounds. | ||
| - **Path alias `@/*` maps to repo root** (`tsconfig.json` paths); prefer `@/src/...` imports over deep relative ones. | ||
| - **HTML from API responses is unsafe** — sanitize with `sanitizeHtml()` from `src/lib/utils.ts` (or DOMPurify) before rendering. The consumer-supplied `transformHtmlToComponents` receives **unsanitized** HTML; that's the consumer's responsibility per the README. | ||
| - **Conventional commits** drive the automated release (`npm run release` → `scripts/release.ts`). `feat:` → minor, `fix:` → patch, `BREAKING CHANGE:` footer → major. Hotfix releases go through `npm run release:fix` from a version tag (see [DEVELOPMENT.md](DEVELOPMENT.md)). | ||
| - **CHANGELOG.md and `package.json` version** are updated by the release scripts — do not bump them by hand on feature branches. | ||
|
|
||
| ## Things that look broken but aren't | ||
|
|
||
| - `dist/` is checked into the working tree during dev (it's gitignored, but `prepare` runs `build`); don't be alarmed by churn there. | ||
| - `src/client/*.gen.ts` regenerates wholesale — diffs will be large after `npm run openapi-ts` runs. | ||
| - The error boundary's `useParentErrorBoundary` defaults to `true` in code but `false` in the README example; the README documents the recommended consumer config, not the default. |
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
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
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,178 @@ | ||
| import { Page, Route } from '@playwright/test'; | ||
|
|
||
| export async function setupVercelBypass(page: Page) { | ||
| await page.route('**/*', async (route: Route) => { | ||
| const url = route.request().url(); | ||
|
|
||
| // Only add Vercel bypass headers to requests to the Vercel deployment | ||
| if (url.includes('vercel.app') || url.includes('localhost:3001')) { | ||
| const headers = { | ||
| ...route.request().headers(), | ||
| 'x-vercel-protection-bypass': process.env.VERCEL_BYPASS_TOKEN || '', | ||
| 'x-vercel-set-bypass-cookie': 'true', | ||
| }; | ||
|
|
||
| await route.continue({ headers }); | ||
| } else { | ||
| // For external requests (like to gateway.remote.com), continue without the headers | ||
| await route.continue(); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| export type inputType = | ||
| | 'textField' | ||
| | 'select' | ||
| | 'comboBox' | ||
| | 'radio' | ||
| | 'checkbox' | ||
| | 'datepicker'; | ||
|
|
||
| export type FillFormOptions = { | ||
| type: inputType; | ||
| value?: string; | ||
| name?: string; | ||
| testId?: string; | ||
| options?: { nativeSelect?: boolean }; | ||
| }; | ||
|
|
||
| export async function fillForm(page: Page, values: FillFormOptions[]) { | ||
| for (const option of values) { | ||
| switch (option.type) { | ||
| case 'textField': | ||
| if (option.name) { | ||
| await fillTextField(page, option.name, option.value); | ||
| } else { | ||
| throw new Error('textField need name to be located'); | ||
| } | ||
| break; | ||
| case 'select': | ||
| if (option.name) { | ||
| await fillSelect(page, option.value, option.name, option.options); | ||
| } else { | ||
| throw new Error('select need name to be located'); | ||
| } | ||
| break; | ||
| case 'comboBox': | ||
| if (option.name) { | ||
| await fillComboBox(page, option.value, option.name); | ||
| } else { | ||
| throw new Error('comboBox need name to be located'); | ||
| } | ||
| break; | ||
| case 'radio': | ||
| if (option.name) { | ||
| await fillRadio(page, option.value, option.name); | ||
| } else { | ||
| throw new Error('radio need name to be located'); | ||
| } | ||
| break; | ||
| case 'checkbox': | ||
| if (option.name) { | ||
| await fillCheckbox(page, option.value, option.name); | ||
| } else { | ||
| throw new Error('checkbox need name to be located'); | ||
| } | ||
| break; | ||
| case 'datepicker': | ||
| if (option.testId) { | ||
| await fillDatepicker(page, option.value, option.testId); | ||
| } else { | ||
| throw new Error('DatePicker need testId to be located'); | ||
| } | ||
| break; | ||
| default: | ||
| throw new Error(`Unsupported input type: ${option.type}`); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| export async function fillTextField( | ||
| page: Page, | ||
| name: string, | ||
| value: string = '', | ||
| ) { | ||
| await page.locator(`[data-field="${name}"] :is(input, textarea)`).fill(value); | ||
| } | ||
|
|
||
| export async function fillSelect( | ||
| page: Page, | ||
| value: string = '', | ||
| name: string, | ||
| options: { nativeSelect?: boolean } = { nativeSelect: false }, | ||
| ) { | ||
| if (options.nativeSelect) { | ||
| const dropdown = page.locator(`[data-field="${name}"] select`); | ||
| await dropdown.waitFor({ state: 'visible' }); | ||
| await dropdown.selectOption(value); | ||
| } else { | ||
| const dropdown = page.locator(`[data-field="${name}"]`); | ||
| await dropdown.click(); | ||
| const option = page.getByRole('option', { name: value }); | ||
| await option.waitFor({ state: 'visible' }); | ||
| await option.dispatchEvent('click'); | ||
| } | ||
| } | ||
|
|
||
| export async function fillComboBox( | ||
| page: Page, | ||
| value: string = '', | ||
| dataField: string, | ||
| ) { | ||
| await page | ||
| .locator(`[data-field="${dataField}"]`) | ||
| .getByRole('combobox') | ||
| .click(); | ||
| const categoryOption = page.getByRole('option', { | ||
| name: value, | ||
| }); | ||
| await categoryOption.waitFor({ state: 'visible' }); | ||
| await categoryOption.click(); | ||
| } | ||
|
|
||
| export async function fillRadio( | ||
| page: Page, | ||
| value: string = '', | ||
| dataField: string, | ||
| ) { | ||
| const locator = page.locator( | ||
| `[data-field="${dataField}"] button[role="radio"][value="${value}"]`, | ||
| ); | ||
| await locator.waitFor({ state: 'visible' }); | ||
| await locator.click(); | ||
| } | ||
|
|
||
| export async function fillCheckbox( | ||
| page: Page, | ||
| value: string = '', | ||
| dataField: string, | ||
| ) { | ||
| const locator = page.locator( | ||
| `[data-field="${dataField}"] button[role="checkbox"]`, | ||
| ); | ||
| await locator.waitFor({ state: 'visible' }); | ||
| await locator.click(); | ||
| } | ||
|
cursor[bot] marked this conversation as resolved.
|
||
|
|
||
| export async function fillDatepicker( | ||
| page: Page, | ||
| value: string = '', | ||
| testId: string, | ||
| ) { | ||
| await page.getByTestId(testId).click(); | ||
| if (value === 'auto') { | ||
| await page | ||
| .locator('button[role="gridcell"]:not([disabled])') | ||
| .first() | ||
| .click(); | ||
| } else { | ||
| await page | ||
| .getByRole('button', { | ||
| name: value, | ||
| exact: true, | ||
| }) | ||
| .and(page.locator(':not([disabled])')) | ||
| .first() | ||
| .click(); | ||
| } | ||
| } | ||
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.
Uh oh!
There was an error while loading. Please reload this page.