Skip to content

Conversation

@jeffredodd
Copy link
Contributor

Summary

This PR introduces Playwright for end-to-end testing of SDK flows.

What's included

Infrastructure

  • Playwright configuration with Chromium browser support
  • E2E test app (e2e/) with its own Vite config and MSW mocking
  • CI integration - new e2e job in GitHub Actions that:
    • Installs Playwright browsers
    • Initializes MSW
    • Runs e2e tests
    • Uploads test reports as artifacts

Test Coverage

Initial tests for all major flows:

  • Company Onboarding (4 tests)
  • Employee Onboarding (1 test)
  • Employee Self-Onboarding (1 test)
  • Contractor Onboarding (3 tests)
  • Contractor Payments (2 tests)
  • Payroll (4 tests)

Running Locally

# Run all e2e tests
npm run test:e2e

# Run with UI mode for debugging
npm run test:e2e:ui

# Run only the e2e dev server
npm run e2e:serve

Why Playwright?

  • Fast execution with parallel testing
  • Built-in browser automation
  • Excellent debugging with traces/videos on failure
  • Works well with MSW for API mocking
  • Good CI integration with artifact uploads

@jeffredodd jeffredodd force-pushed the feature/playwright-e2e-tests branch from 34329e6 to 9c0ff34 Compare February 3, 2026 23:55
@jeffredodd jeffredodd marked this pull request as ready for review February 3, 2026 23:55
Copilot AI review requested due to automatic review settings February 3, 2026 23:55
@jeffredodd jeffredodd force-pushed the feature/playwright-e2e-tests branch from 9c0ff34 to bd1e33a Compare February 3, 2026 23:56
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces Playwright for end-to-end testing of SDK flows, providing comprehensive test coverage across major features including company onboarding, employee onboarding, contractor management, and payroll processing.

Changes:

  • Added Playwright infrastructure with CI integration and test harness app
  • Extended MSW mock handlers to support e2e test scenarios
  • Created 15 e2e tests covering 6 major flows

Reviewed changes

Copilot reviewed 24 out of 25 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
playwright.config.ts Configures Playwright with Chromium browser, test directory, and dev server settings
package.json Adds e2e test scripts and MSW worker directory configuration
eslint.config.mjs Excludes e2e directory from linting
vite.config.ts Excludes e2e directory from unit test collection
.gitignore Ignores Playwright test artifacts and MSW service worker file
.github/workflows/ci.yaml Adds e2e job to CI pipeline with Playwright browser installation and test execution
e2e/vite.config.ts Configures Vite build for e2e test app with React and SCSS support
e2e/index.html Provides HTML entry point for e2e test harness
e2e/main.tsx Implements flow renderer that loads different SDK flows based on URL parameters
e2e/mocks/browser.ts Sets up MSW browser worker for API mocking in e2e tests
e2e/tests/*.spec.ts Implements e2e tests for company onboarding, employee onboarding, contractor flows, and payroll
src/test/mocks/handlers.ts Exports additional handlers and imports new mock modules for e2e support
src/test/mocks/apis/*.ts Adds and updates mock API handlers to support e2e test scenarios

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@jeffredodd jeffredodd force-pushed the feature/playwright-e2e-tests branch from 5cb4d4f to da31686 Compare February 4, 2026 17:10
Copy link
Member

@serikjensen serikjensen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good! couple of comments

Also, question: are we removing the existing tests while we add these ones to playwright? or will that be a fast follow? Just worried about having both side by side

Comment on lines 1 to 35
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import svgr from 'vite-plugin-svgr'
import { resolve } from 'path'

export default defineConfig({
root: resolve(__dirname),
publicDir: resolve(__dirname, 'public'),
plugins: [
react(),
svgr({
svgrOptions: {
exportType: 'default',
titleProp: true,
},
include: ['**/*.svg?react', '**/*.svg'],
}),
],
resolve: {
alias: {
'@': resolve(__dirname, '../src'),
},
},
css: {
preprocessorOptions: {
scss: {
api: 'modern-compiler',
additionalData: `@use "@/styles/Helpers" as *; @use '@/styles/Responsive' as *;\n`,
},
},
},
server: {
port: 5173,
},
})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we update this to share the existing vite config where possible so it can scale?

Comment on lines +168 to +178
const getLabel = () => {
if (paymentType === 'Payroll') {
return payrollRange
? t('selectLabelPayroll', { payrollRange })
: t('selectFallback')
}
return t('selectFallback')
}

return {
label:
paymentType === 'Payroll'
? payrollRange
? t('selectLabelPayroll', { payrollRange })
: t('selectFallback')
: paymentType === 'ContractorPaymentGroup'
? t('selectLabelContractorPaymentGroup', {
requestedAmount: formatCurrency(Number(requestedAmount)),
})
: t('selectFallback'),
label: getLabel(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we know why it is making changes here? Seems like if there's an issue this one should be a sep PR

@jeffredodd jeffredodd marked this pull request as draft February 8, 2026 22:29
jeffredodd and others added 2 commits February 10, 2026 09:05
Add support for running Playwright e2e tests against real APIs in
addition to MSW mocks. Three modes are now available:

- `npm run test:e2e` -- MSW mocks (unchanged, used in CI)
- `npm run test:e2e:local` -- local GWS-Flows + ZP instance
- `npm run test:e2e:demo` -- live demo env (flows.gusto-demo.com)

The demo mode auto-provisions a fresh company via GWS-Flows, creates
test entities (employees, contractors, locations), and refreshes
expired tokens automatically. Tests are data-agnostic and work with
dynamically generated data.

Adds an `e2e-demo` CI job that runs the full suite against the demo
environment on every push.

Co-authored-by: Cursor <cursoragent@cursor.com>
The CI runner cannot reach flows.gusto-demo.com (likely requires
VPN or internal network access). Mark the job as non-blocking until
a runner with the appropriate network access is configured.

Co-authored-by: Cursor <cursoragent@cursor.com>
Copy link

@boostsecurity-io-ai boostsecurity-io-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️  2 New Security Findings

The latest commit contains 2 new security findings.

Findings Note: 2 findings are displayed as inline comments.

Not a finding? Ignore it by adding a comment on the line with just the word noboost.

Scanner: boostsecurity - Semgrep

}

async function postToApi<T>(endpoint: string, data: Record<string, unknown>): Promise<T> {
const response = await fetch(`${GWS_FLOWS_BASE}${endpoint}`, {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CWE-918: Server-Side Request Forgery (SSRF)
Original Rule ID: rules_lgpl_javascript_ssrf_rule-node-ssrf

Details

The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.

This rule detected user-controlled URLs being passed to Node.js HTTP client
libraries including axios.get(), axios.post(), fetch(), http.get(),
http.request(), needle(), request(), urllib.request(),
superagent.get(), bent(), got.get(), net.connect(), and
socket.io-client.io(). When user input controls the destination URL of HTTP
requests without validation, Server-Side Request Forgery (SSRF) vulnerabilities
arise. SSRF allows attackers to force the server to make requests to internal
systems, cloud metadata endpoints (such as 169.254.169.254), or other
unauthorized destinations. This can expose internal APIs, databases,
administrative panels, or enable network scanning and pivoting attacks that
bypass firewall rules and network segmentation.
 📘 Learn More

AI Remediation

Input validation using a strict regular expression was added to ensure that only safe, relative path endpoints (starting with '/') are accepted in fetchFromApi and postToApi. This helps prevent Server-Side Request Forgery (SSRF) by blocking user-controlled or malicious URLs from being used in internal HTTP requests. The fix enforces that only valid API endpoints on the intended host can be called, reducing the SSRF risk.

At line 91, do the following changes:

 }
 
 async function fetchFromApi<T>(endpoint: string): Promise<T> {
+  if (!/^\/[\w\-/.]+$/.test(endpoint)) {
+    throw new Error('Invalid endpoint path');
+  }
   const response = await fetch(`${GWS_FLOWS_BASE}${endpoint}`)
   if (!response.ok) {
     throw new Error(`API request failed: ${response.status} ${response.statusText}`)

At line 99, do the following changes:

 }
 
 async function postToApi<T>(endpoint: string, data: Record<string, unknown>): Promise<T> {
+  if (!/^\/[\w\-/.]+$/.test(endpoint)) {
+    throw new Error('Invalid endpoint path');
+  }
   const response = await fetch(`${GWS_FLOWS_BASE}${endpoint}`, {
     method: 'POST',
     headers: { 'Content-Type': 'application/json' },

async function testToken(flowToken: string, companyId: string): Promise<boolean> {
try {
const response = await fetch(
`${GWS_FLOWS_BASE}/fe_sdk/${flowToken}/v1/companies/${companyId}/locations`,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CWE-918: Server-Side Request Forgery (SSRF)
Original Rule ID: rules_lgpl_javascript_ssrf_rule-node-ssrf

Details

The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.

This rule detected user-controlled URLs being passed to Node.js HTTP client
libraries including axios.get(), axios.post(), fetch(), http.get(),
http.request(), needle(), request(), urllib.request(),
superagent.get(), bent(), got.get(), net.connect(), and
socket.io-client.io(). When user input controls the destination URL of HTTP
requests without validation, Server-Side Request Forgery (SSRF) vulnerabilities
arise. SSRF allows attackers to force the server to make requests to internal
systems, cloud metadata endpoints (such as 169.254.169.254), or other
unauthorized destinations. This can expose internal APIs, databases,
administrative panels, or enable network scanning and pivoting attacks that
bypass firewall rules and network segmentation.
 📘 Learn More

AI Remediation

All instances where user-controlled URLs were sent to fetch() now include strict protocol/domain validation and local/internal IP restriction before any HTTP request. This mitigates SSRF risks by ensuring that dangerous or internal-address URLs are never requested. The fix enforces only http(s) schemes and blocks private, loopback, and cloud metadata endpoints to prevent server-side network abuse.

At line 66, do the following changes:

       }
 
       try {
+        // Validate demoPageUrl before making request to prevent SSRF
+        if (!/^https?:\/\//.test(demoPageUrl) || /^(https?:\/\/)?(localhost|127\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|169\.254\.169\.254)/.test(demoPageUrl)) {
+          throw new Error('Blocked potentially unsafe url');
+        }
         const response = await fetch(demoPageUrl, {
           headers: { Accept: 'text/html' },
           signal: AbortSignal.timeout(10000),

At line 100, do the following changes:

 
     if (!companyId) {
       try {
-        const companyResponse = await fetch(`${GWS_FLOWS_BASE}/fe_sdk/${flowToken}/v1/companies`, {
+        // Validate URL before making request to prevent SSRF
+        const companiesUrl = `${GWS_FLOWS_BASE}/fe_sdk/${flowToken}/v1/companies`;
+        if (!/^https?:\/\//.test(companiesUrl) || /^(https?:\/\/)?(localhost|127\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|169\.254\.169\.254)/.test(companiesUrl)) {
+          throw new Error('Blocked potentially unsafe url');
+        }
+        const companyResponse = await fetch(companiesUrl, {
           signal: AbortSignal.timeout(10000),
         })
         if (companyResponse.ok) {

At line 187, do the following changes:

 
 async function testToken(flowToken: string, companyId: string): Promise<boolean> {
   try {
+    const locationsUrl = `${GWS_FLOWS_BASE}/fe_sdk/${flowToken}/v1/companies/${companyId}/locations`;
+    // Validate URL before making request to prevent SSRF
+    if (!/^https?:\/\//.test(locationsUrl) || /^(https?:\/\/)?(localhost|127\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|169\.254\.169\.254)/.test(locationsUrl)) {
+      throw new Error('Blocked potentially unsafe url');
+    }
     const response = await fetch(
-      `${GWS_FLOWS_BASE}/fe_sdk/${flowToken}/v1/companies/${companyId}/locations`,
+      locationsUrl,
       { signal: AbortSignal.timeout(5000) },
     )
     return response.ok

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants