chore: Jest to vitestga!
This commit is contained in:
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": ["babel-preset-vite"]
|
|
||||||
}
|
|
@ -1,299 +0,0 @@
|
|||||||
// Test runner
|
|
||||||
import {expect, test, beforeAll, afterEach, afterAll} from '@jest/globals';
|
|
||||||
|
|
||||||
// Render mocking, DOM querying
|
|
||||||
import {act, render, within} from '@testing-library/react'
|
|
||||||
import '@testing-library/jest-dom' // Required for .toHaveStyle
|
|
||||||
|
|
||||||
// Request mocking
|
|
||||||
import {http, HttpResponse} from 'msw'
|
|
||||||
import {setupServer} from 'msw/node'
|
|
||||||
|
|
||||||
// React and XState code to test
|
|
||||||
import { Models } from '@kittycad/lib'
|
|
||||||
import { createActor } from 'xstate'
|
|
||||||
import {
|
|
||||||
BillingRemaining,
|
|
||||||
BillingRemainingMode,
|
|
||||||
} from '@src/components/BillingRemaining'
|
|
||||||
import { BillingDialog, } from '@src/components/BillingDialog'
|
|
||||||
import { BILLING_CONTEXT_DEFAULTS, billingMachine, BillingTransition } from '@src/machines/billingMachine'
|
|
||||||
|
|
||||||
// Setup basic request mocking
|
|
||||||
const server = setupServer()
|
|
||||||
beforeAll(() => server.listen())
|
|
||||||
afterEach(() => server.resetHandlers())
|
|
||||||
afterAll(() => server.close())
|
|
||||||
|
|
||||||
// Data ripped from docs.zoo.dev
|
|
||||||
const createUserPaymentBalanceResponse = (opts: {
|
|
||||||
monthlyApiCreditsRemaining,
|
|
||||||
stableApiCreditsRemaining,
|
|
||||||
}): Models['CustomerBalance_type'] => ({
|
|
||||||
"created_at": "2025-05-05T16:05:47.317Z",
|
|
||||||
"id": "de607b7e-90ba-4977-8561-16e8a9ea0e50",
|
|
||||||
"map_id": "d7f7de34-9bc3-4b8b-9951-cdee03fc792d",
|
|
||||||
"modeling_app_enterprise_price": {
|
|
||||||
"type": "enterprise"
|
|
||||||
},
|
|
||||||
"monthly_api_credits_remaining": opts.monthlyApiCreditsRemaining,
|
|
||||||
"monthly_api_credits_remaining_monetary_value": "22.47",
|
|
||||||
"stable_api_credits_remaining": opts.stableApiCreditsRemaining,
|
|
||||||
"stable_api_credits_remaining_monetary_value": "18.91",
|
|
||||||
"subscription_details": undefined,
|
|
||||||
"subscription_id": "Hnd3jalJkHA3lb1YexOTStZtPYHTM",
|
|
||||||
"total_due": "100.08",
|
|
||||||
"updated_at": "2025-05-05T16:05:47.317Z"
|
|
||||||
})
|
|
||||||
|
|
||||||
const createOrgResponse = (opts: {
|
|
||||||
}): Models['Org_type'] => ({
|
|
||||||
"allow_users_in_domain_to_auto_join": true,
|
|
||||||
"billing_email": "m@dN9MCH.com",
|
|
||||||
"billing_email_verified": "2025-05-05T18:52:02.021Z",
|
|
||||||
"block": "payment_method_failed",
|
|
||||||
"can_train_on_data": true,
|
|
||||||
"created_at": "2025-05-05T18:52:02.021Z",
|
|
||||||
"domain": "Ctxde1hpG8xTvvlef5SEPm7",
|
|
||||||
"id": "78432284-8660-46bf-ac65-d00bf9b18c3e",
|
|
||||||
"image": "https://Rt0.yK.com/R2SoRtl/tpUdckyDJ",
|
|
||||||
"name": "AevRR4w42KdkA487dh",
|
|
||||||
"phone": "+1-696-641-2790",
|
|
||||||
"stripe_id": "sCfjVscpLyOBYUWO7Vlx",
|
|
||||||
"updated_at": "2025-05-05T18:52:02.021Z"
|
|
||||||
})
|
|
||||||
|
|
||||||
const createUserPaymentSubscriptionsResponse = (opts: {
|
|
||||||
monthlyPayAsYouGoApiCreditsTotal,
|
|
||||||
name,
|
|
||||||
}): Models['ZooProductSubscriptions_type'] => ({
|
|
||||||
"modeling_app": {
|
|
||||||
"annual_discount": 10,
|
|
||||||
"description": "1ztERftrU3L3yOnv5epTLcM",
|
|
||||||
"endpoints_included": [
|
|
||||||
"modeling"
|
|
||||||
],
|
|
||||||
"features": [
|
|
||||||
{
|
|
||||||
"info": "zZcZKHejXabT5HMZDkSkDGD2bfzkAt"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"monthly_pay_as_you_go_api_credits": opts.monthlyPayAsYouGoApiCreditsTotal,
|
|
||||||
"monthly_pay_as_you_go_api_credits_monetary_value": "55.85",
|
|
||||||
"name": opts.name,
|
|
||||||
"pay_as_you_go_api_credit_price": "18.49",
|
|
||||||
"price": {
|
|
||||||
"interval": "year",
|
|
||||||
"price": "50.04",
|
|
||||||
"type": "per_user"
|
|
||||||
},
|
|
||||||
"share_links": [
|
|
||||||
"password_protected"
|
|
||||||
],
|
|
||||||
"support_tier": "community",
|
|
||||||
"training_data_behavior": "default_on",
|
|
||||||
"type": {
|
|
||||||
"saml_sso": true,
|
|
||||||
"type": "organization"
|
|
||||||
},
|
|
||||||
"zoo_tools_included": [
|
|
||||||
"text_to_cad"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Shows a loading spinner when uninitialized credit count', async () => {
|
|
||||||
server.use(
|
|
||||||
http.get('*/user/payment/balance', (req, res, ctx) => {
|
|
||||||
return HttpResponse.json({})
|
|
||||||
}),
|
|
||||||
http.get('*/user/payment/subscriptions', (req, res, ctx) => {
|
|
||||||
return HttpResponse.json({})
|
|
||||||
}),
|
|
||||||
http.get('*/org', (req, res, ctx) => {
|
|
||||||
return new HttpResponse(403)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const billingActor = createActor(billingMachine, { input: BILLING_CONTEXT_DEFAULTS }).start()
|
|
||||||
|
|
||||||
const { queryByTestId } = render(<BillingRemaining
|
|
||||||
mode={BillingRemainingMode.ProgressBarFixed}
|
|
||||||
billingActor={billingActor}
|
|
||||||
/>)
|
|
||||||
|
|
||||||
await expect(queryByTestId('spinner')).toBeVisible()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Shows the total credits for Unknown subscription', async () => {
|
|
||||||
const data = {
|
|
||||||
balance: {
|
|
||||||
monthlyApiCreditsRemaining: 10,
|
|
||||||
stableApiCreditsRemaining: 25,
|
|
||||||
},
|
|
||||||
subscriptions: {
|
|
||||||
monthlyPayAsYouGoApiCreditsTotal: 20,
|
|
||||||
name: "unknown",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
server.use(
|
|
||||||
http.get('*/user/payment/balance', (req, res, ctx) => {
|
|
||||||
return HttpResponse.json(createUserPaymentBalanceResponse(data.balance))
|
|
||||||
}),
|
|
||||||
http.get('*/user/payment/subscriptions', (req, res, ctx) => {
|
|
||||||
return HttpResponse.json(createUserPaymentSubscriptionsResponse(data.subscriptions))
|
|
||||||
}),
|
|
||||||
http.get('*/org', (req, res, ctx) => {
|
|
||||||
return new HttpResponse(403)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const billingActor = createActor(billingMachine, { input: BILLING_CONTEXT_DEFAULTS }).start()
|
|
||||||
|
|
||||||
const { queryByTestId } = render(<BillingRemaining
|
|
||||||
mode={BillingRemainingMode.ProgressBarFixed}
|
|
||||||
billingActor={billingActor}
|
|
||||||
/>)
|
|
||||||
|
|
||||||
await act(() => {
|
|
||||||
billingActor.send({ type: BillingTransition.Update, apiToken: "it doesn't matter wtf this is :)" })
|
|
||||||
})
|
|
||||||
|
|
||||||
const totalCredits = data.balance.monthlyApiCreditsRemaining + data.balance.stableApiCreditsRemaining
|
|
||||||
await expect(billingActor.getSnapshot().context.credits).toBe(totalCredits)
|
|
||||||
await within(queryByTestId('billing-credits')).getByText(totalCredits)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Progress bar reflects ratio left of Free subscription', async () => {
|
|
||||||
const data = {
|
|
||||||
balance: {
|
|
||||||
monthlyApiCreditsRemaining: 10,
|
|
||||||
stableApiCreditsRemaining: 0,
|
|
||||||
},
|
|
||||||
subscriptions: {
|
|
||||||
monthlyPayAsYouGoApiCreditsTotal: 20,
|
|
||||||
name: "free",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
server.use(
|
|
||||||
http.get('*/user/payment/balance', (req, res, ctx) => {
|
|
||||||
return HttpResponse.json(createUserPaymentBalanceResponse(data.balance))
|
|
||||||
}),
|
|
||||||
http.get('*/user/payment/subscriptions', (req, res, ctx) => {
|
|
||||||
return HttpResponse.json(createUserPaymentSubscriptionsResponse(data.subscriptions))
|
|
||||||
}),
|
|
||||||
http.get('*/org', (req, res, ctx) => {
|
|
||||||
return new HttpResponse(403)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const billingActor = createActor(billingMachine, { input: BILLING_CONTEXT_DEFAULTS }).start()
|
|
||||||
|
|
||||||
const { queryByTestId } = render(<BillingRemaining
|
|
||||||
mode={BillingRemainingMode.ProgressBarFixed}
|
|
||||||
billingActor={billingActor}
|
|
||||||
/>)
|
|
||||||
|
|
||||||
await act(() => {
|
|
||||||
billingActor.send({ type: BillingTransition.Update, apiToken: "it doesn't matter wtf this is :)" })
|
|
||||||
})
|
|
||||||
|
|
||||||
const totalCredits = data.balance.monthlyApiCreditsRemaining + data.balance.stableApiCreditsRemaining
|
|
||||||
const monthlyCredits = data.subscriptions.monthlyPayAsYouGoApiCreditsTotal
|
|
||||||
const context = billingActor.getSnapshot().context
|
|
||||||
await expect(context.credits).toBe(totalCredits)
|
|
||||||
await expect(context.allowance).toBe(monthlyCredits)
|
|
||||||
|
|
||||||
await within(queryByTestId('billing-credits')).getByText(totalCredits)
|
|
||||||
await expect(queryByTestId('billing-remaining-progress-bar-inner')).toHaveStyle({
|
|
||||||
width: "50.00%"
|
|
||||||
})
|
|
||||||
})
|
|
||||||
test('Shows infinite credits for Pro subscription', async () => {
|
|
||||||
const data = {
|
|
||||||
// These are all ignored
|
|
||||||
balance: {
|
|
||||||
monthlyApiCreditsRemaining: 10,
|
|
||||||
stableApiCreditsRemaining: 0,
|
|
||||||
},
|
|
||||||
subscriptions: {
|
|
||||||
// This should be ignored because it's Pro tier.
|
|
||||||
monthlyPayAsYouGoApiCreditsTotal: 20,
|
|
||||||
name: "pro",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
server.use(
|
|
||||||
http.get('*/user/payment/balance', (req, res, ctx) => {
|
|
||||||
return HttpResponse.json(createUserPaymentBalanceResponse(data.balance))
|
|
||||||
}),
|
|
||||||
http.get('*/user/payment/subscriptions', (req, res, ctx) => {
|
|
||||||
return HttpResponse.json(createUserPaymentSubscriptionsResponse(data.subscriptions))
|
|
||||||
}),
|
|
||||||
http.get('*/org', (req, res, ctx) => {
|
|
||||||
return new HttpResponse(403)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const billingActor = createActor(billingMachine, { input: BILLING_CONTEXT_DEFAULTS }).start()
|
|
||||||
|
|
||||||
const { queryByTestId } = render(<BillingRemaining
|
|
||||||
mode={BillingRemainingMode.ProgressBarFixed}
|
|
||||||
billingActor={billingActor}
|
|
||||||
/>)
|
|
||||||
|
|
||||||
await act(() => {
|
|
||||||
billingActor.send({ type: BillingTransition.Update, apiToken: "aosetuhsatuh" })
|
|
||||||
})
|
|
||||||
|
|
||||||
await expect(queryByTestId('infinity')).toBeVisible()
|
|
||||||
// You can't do `.not.toBeVisible` folks. When the query fails it's because
|
|
||||||
// no element could be found. toBeVisible should be used on an element
|
|
||||||
// that's found but may not be visible due to `display` or others.
|
|
||||||
await expect(queryByTestId('billing-remaining-progress-bar-inline')).toBe(null)
|
|
||||||
})
|
|
||||||
test('Shows infinite credits for Enterprise subscription', async () => {
|
|
||||||
const data = {
|
|
||||||
// These are all ignored, user is part of an org.
|
|
||||||
balance: {
|
|
||||||
monthlyApiCreditsRemaining: 10,
|
|
||||||
stableApiCreditsRemaining: 0,
|
|
||||||
},
|
|
||||||
subscriptions: {
|
|
||||||
// This should be ignored because it's Pro tier.
|
|
||||||
monthlyPayAsYouGoApiCreditsTotal: 20,
|
|
||||||
// This should be ignored because the user is part of an Org.
|
|
||||||
name: "free",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
server.use(
|
|
||||||
http.get('*/user/payment/balance', (req, res, ctx) => {
|
|
||||||
return HttpResponse.json(createUserPaymentBalanceResponse(data.balance))
|
|
||||||
}),
|
|
||||||
http.get('*/user/payment/subscriptions', (req, res, ctx) => {
|
|
||||||
return HttpResponse.json(createUserPaymentSubscriptionsResponse(data.subscriptions))
|
|
||||||
}),
|
|
||||||
// Ok finally the first use of an org lol
|
|
||||||
http.get('*/org', (req, res, ctx) => {
|
|
||||||
return HttpResponse.json(createOrgResponse())
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const billingActor = createActor(billingMachine, { input: BILLING_CONTEXT_DEFAULTS }).start()
|
|
||||||
|
|
||||||
const { queryByTestId } = render(<BillingRemaining
|
|
||||||
mode={BillingRemainingMode.ProgressBarFixed}
|
|
||||||
billingActor={billingActor}
|
|
||||||
/>)
|
|
||||||
|
|
||||||
await act(() => {
|
|
||||||
billingActor.send({ type: BillingTransition.Update, apiToken: "aosetuhsatuh" })
|
|
||||||
})
|
|
||||||
|
|
||||||
// The result should be the same as Pro users.
|
|
||||||
await expect(queryByTestId('infinity')).toBeVisible()
|
|
||||||
await expect(queryByTestId('billing-remaining-progress-bar-inline')).toBe(null)
|
|
||||||
})
|
|
@ -1,25 +0,0 @@
|
|||||||
import { pathsToModuleNameMapper } from 'ts-jest'
|
|
||||||
// In the following statement, replace `./tsconfig` with the path to your `tsconfig` file
|
|
||||||
// which contains the path mapping (ie the `compilerOptions.paths` option):
|
|
||||||
import { compilerOptions } from './tsconfig.json'
|
|
||||||
import type { Config } from 'jest'
|
|
||||||
|
|
||||||
const jestConfig: Config = {
|
|
||||||
// [...]
|
|
||||||
preset: "ts-jest",
|
|
||||||
transform: {
|
|
||||||
"^.+\.tsx?$": ["ts-jest",{ babelConfig: true }],
|
|
||||||
},
|
|
||||||
testEnvironment: "jest-fixed-jsdom",
|
|
||||||
// Include both standard test patterns and our custom .jesttest. pattern
|
|
||||||
testMatch: [
|
|
||||||
"**/__tests__/**/*.[jt]s?(x)",
|
|
||||||
"**/?(*.)+(spec|test).[tj]s?(x)",
|
|
||||||
"**/?(*.)+(jesttest).[tj]s?(x)"
|
|
||||||
],
|
|
||||||
// TAG: paths, path, baseUrl, alias
|
|
||||||
// This is necessary to use tsconfig path aliases.
|
|
||||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '<rootDir>/../' }),
|
|
||||||
}
|
|
||||||
|
|
||||||
export default jestConfig
|
|
@ -1,35 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"noErrorTruncation": true,
|
|
||||||
"baseUrl": "../../",
|
|
||||||
"paths": {
|
|
||||||
"@kittycad/codemirror-lsp-client": [
|
|
||||||
"./packages/codemirror-lsp-client/src/index.ts"
|
|
||||||
],
|
|
||||||
"@kittycad/codemirror-lang-kcl": [
|
|
||||||
"./packages/codemirror-lang-kcl/src/index.ts"
|
|
||||||
],
|
|
||||||
"@rust/*": ["./rust/*"],
|
|
||||||
"@e2e/*": ["./e2e/*"],
|
|
||||||
"@src/*": ["./src/*"],
|
|
||||||
"@public/*": ["./public/*"],
|
|
||||||
"@root/*": ["./*"]
|
|
||||||
},
|
|
||||||
"target": "esnext",
|
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
|
||||||
"allowJs": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"module": "commonjs",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"composite": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"jsx": "react-jsx"
|
|
||||||
}
|
|
||||||
}
|
|
@ -131,8 +131,7 @@
|
|||||||
"test": "vitest --mode development",
|
"test": "vitest --mode development",
|
||||||
"test:rust": "(cd rust && just test && just lint)",
|
"test:rust": "(cd rust && just test && just lint)",
|
||||||
"test:snapshots": "cross-env TARGET=web NODE_ENV=development playwright test --config=playwright.config.ts --grep=@snapshot --trace=on",
|
"test:snapshots": "cross-env TARGET=web NODE_ENV=development playwright test --config=playwright.config.ts --grep=@snapshot --trace=on",
|
||||||
"test:unit": "vitest run --mode development --exclude **/jest-component-unit-tests/*",
|
"test:unit": "vitest run --mode development",
|
||||||
"test:unit:components": "jest -c jest-component-unit-tests/jest.config.ts --rootDir jest-component-unit-tests/",
|
|
||||||
"test:e2e:web": "cross-env TARGET=web NODE_ENV=development playwright test --config=playwright.config.ts --project=\"Google Chrome\" --grep=@web",
|
"test:e2e:web": "cross-env TARGET=web NODE_ENV=development playwright test --config=playwright.config.ts --project=\"Google Chrome\" --grep=@web",
|
||||||
"test:e2e:desktop": "cross-env TARGET=desktop playwright test --config=playwright.electron.config.ts --grep-invert=\"@snapshot|@web\"",
|
"test:e2e:desktop": "cross-env TARGET=desktop playwright test --config=playwright.electron.config.ts --grep-invert=\"@snapshot|@web\"",
|
||||||
"test:e2e:desktop:local": "cross-env TARGET=desktop npm run tronb:vite:dev && playwright test --config=playwright.electron.config.ts --grep-invert=\"@snapshot|@web\" --grep-invert=\"$(curl --silent https://test-analysis-bot.hawk-dinosaur.ts.net/projects/KittyCAD/modeling-app/tests/disabled/regex)\"",
|
"test:e2e:desktop:local": "cross-env TARGET=desktop npm run tronb:vite:dev && playwright test --config=playwright.electron.config.ts --grep-invert=\"@snapshot|@web\" --grep-invert=\"$(curl --silent https://test-analysis-bot.hawk-dinosaur.ts.net/projects/KittyCAD/modeling-app/tests/disabled/regex)\"",
|
||||||
|
347
src/components/billing.test.tsx
Normal file
347
src/components/billing.test.tsx
Normal file
@ -0,0 +1,347 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
// This was moved out of the Jest workflow. Needs proper TS TLC.
|
||||||
|
|
||||||
|
// Test runner
|
||||||
|
|
||||||
|
// Render mocking, DOM querying
|
||||||
|
import { act, render, within } from '@testing-library/react'
|
||||||
|
import '@testing-library/jest-dom' // Required for .toHaveStyle
|
||||||
|
|
||||||
|
// Request mocking
|
||||||
|
import { http, HttpResponse } from 'msw'
|
||||||
|
import { setupServer } from 'msw/node'
|
||||||
|
|
||||||
|
// React and XState code to test
|
||||||
|
import type { Models } from '@kittycad/lib'
|
||||||
|
import { createActor } from 'xstate'
|
||||||
|
import {
|
||||||
|
BillingRemaining,
|
||||||
|
BillingRemainingMode,
|
||||||
|
} from '@src/components/BillingRemaining'
|
||||||
|
import {
|
||||||
|
BILLING_CONTEXT_DEFAULTS,
|
||||||
|
billingMachine,
|
||||||
|
BillingTransition,
|
||||||
|
} from '@src/machines/billingMachine'
|
||||||
|
|
||||||
|
// Setup basic request mocking
|
||||||
|
const server = setupServer()
|
||||||
|
beforeAll(() => server.listen())
|
||||||
|
afterEach(() => server.resetHandlers())
|
||||||
|
afterAll(() => server.close())
|
||||||
|
|
||||||
|
// Data ripped from docs.zoo.dev
|
||||||
|
const createUserPaymentBalanceResponse = (opts: {
|
||||||
|
monthlyApiCreditsRemaining
|
||||||
|
stableApiCreditsRemaining
|
||||||
|
}): Models['CustomerBalance_type'] => ({
|
||||||
|
created_at: '2025-05-05T16:05:47.317Z',
|
||||||
|
id: 'de607b7e-90ba-4977-8561-16e8a9ea0e50',
|
||||||
|
map_id: 'd7f7de34-9bc3-4b8b-9951-cdee03fc792d',
|
||||||
|
modeling_app_enterprise_price: {
|
||||||
|
type: 'enterprise',
|
||||||
|
},
|
||||||
|
monthly_api_credits_remaining: opts.monthlyApiCreditsRemaining,
|
||||||
|
monthly_api_credits_remaining_monetary_value: '22.47',
|
||||||
|
stable_api_credits_remaining: opts.stableApiCreditsRemaining,
|
||||||
|
stable_api_credits_remaining_monetary_value: '18.91',
|
||||||
|
subscription_details: undefined,
|
||||||
|
subscription_id: 'Hnd3jalJkHA3lb1YexOTStZtPYHTM',
|
||||||
|
total_due: '100.08',
|
||||||
|
updated_at: '2025-05-05T16:05:47.317Z',
|
||||||
|
})
|
||||||
|
|
||||||
|
const createOrgResponse = (): Models['Org_type'] => ({
|
||||||
|
allow_users_in_domain_to_auto_join: true,
|
||||||
|
billing_email: 'm@dN9MCH.com',
|
||||||
|
billing_email_verified: '2025-05-05T18:52:02.021Z',
|
||||||
|
block: 'payment_method_failed',
|
||||||
|
can_train_on_data: true,
|
||||||
|
created_at: '2025-05-05T18:52:02.021Z',
|
||||||
|
domain: 'Ctxde1hpG8xTvvlef5SEPm7',
|
||||||
|
id: '78432284-8660-46bf-ac65-d00bf9b18c3e',
|
||||||
|
image: 'https://Rt0.yK.com/R2SoRtl/tpUdckyDJ',
|
||||||
|
name: 'AevRR4w42KdkA487dh',
|
||||||
|
phone: '+1-696-641-2790',
|
||||||
|
stripe_id: 'sCfjVscpLyOBYUWO7Vlx',
|
||||||
|
updated_at: '2025-05-05T18:52:02.021Z',
|
||||||
|
})
|
||||||
|
|
||||||
|
const createUserPaymentSubscriptionsResponse = (opts: {
|
||||||
|
monthlyPayAsYouGoApiCreditsTotal
|
||||||
|
name
|
||||||
|
}): Models['ZooProductSubscriptions_type'] => ({
|
||||||
|
modeling_app: {
|
||||||
|
annual_discount: 10,
|
||||||
|
description: '1ztERftrU3L3yOnv5epTLcM',
|
||||||
|
endpoints_included: ['modeling'],
|
||||||
|
features: [
|
||||||
|
{
|
||||||
|
info: 'zZcZKHejXabT5HMZDkSkDGD2bfzkAt',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
monthly_pay_as_you_go_api_credits: opts.monthlyPayAsYouGoApiCreditsTotal,
|
||||||
|
monthly_pay_as_you_go_api_credits_monetary_value: '55.85',
|
||||||
|
name: opts.name,
|
||||||
|
pay_as_you_go_api_credit_price: '18.49',
|
||||||
|
price: {
|
||||||
|
interval: 'year',
|
||||||
|
price: '50.04',
|
||||||
|
type: 'per_user',
|
||||||
|
},
|
||||||
|
share_links: ['password_protected'],
|
||||||
|
support_tier: 'community',
|
||||||
|
training_data_behavior: 'default_on',
|
||||||
|
type: {
|
||||||
|
saml_sso: true,
|
||||||
|
type: 'organization',
|
||||||
|
},
|
||||||
|
zoo_tools_included: ['text_to_cad'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Shows a loading spinner when uninitialized credit count', async () => {
|
||||||
|
server.use(
|
||||||
|
http.get('*/user/payment/balance', (req, res, ctx) => {
|
||||||
|
return HttpResponse.json({})
|
||||||
|
}),
|
||||||
|
http.get('*/user/payment/subscriptions', (req, res, ctx) => {
|
||||||
|
return HttpResponse.json({})
|
||||||
|
}),
|
||||||
|
http.get('*/org', (req, res, ctx) => {
|
||||||
|
return new HttpResponse(403)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const billingActor = createActor(billingMachine, {
|
||||||
|
input: BILLING_CONTEXT_DEFAULTS,
|
||||||
|
}).start()
|
||||||
|
|
||||||
|
const { queryByTestId } = render(
|
||||||
|
<BillingRemaining
|
||||||
|
mode={BillingRemainingMode.ProgressBarFixed}
|
||||||
|
billingActor={billingActor}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(queryByTestId('spinner')).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Shows the total credits for Unknown subscription', async () => {
|
||||||
|
const data = {
|
||||||
|
balance: {
|
||||||
|
monthlyApiCreditsRemaining: 10,
|
||||||
|
stableApiCreditsRemaining: 25,
|
||||||
|
},
|
||||||
|
subscriptions: {
|
||||||
|
monthlyPayAsYouGoApiCreditsTotal: 20,
|
||||||
|
name: 'unknown',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
server.use(
|
||||||
|
http.get('*/user/payment/balance', (req, res, ctx) => {
|
||||||
|
return HttpResponse.json(createUserPaymentBalanceResponse(data.balance))
|
||||||
|
}),
|
||||||
|
http.get('*/user/payment/subscriptions', (req, res, ctx) => {
|
||||||
|
return HttpResponse.json(
|
||||||
|
createUserPaymentSubscriptionsResponse(data.subscriptions)
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
http.get('*/org', (req, res, ctx) => {
|
||||||
|
return new HttpResponse(403)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const billingActor = createActor(billingMachine, {
|
||||||
|
input: BILLING_CONTEXT_DEFAULTS,
|
||||||
|
}).start()
|
||||||
|
|
||||||
|
const { queryByTestId } = render(
|
||||||
|
<BillingRemaining
|
||||||
|
mode={BillingRemainingMode.ProgressBarFixed}
|
||||||
|
billingActor={billingActor}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
await act(() => {
|
||||||
|
billingActor.send({
|
||||||
|
type: BillingTransition.Update,
|
||||||
|
apiToken: "it doesn't matter wtf this is :)",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalCredits =
|
||||||
|
data.balance.monthlyApiCreditsRemaining +
|
||||||
|
data.balance.stableApiCreditsRemaining
|
||||||
|
await expect(billingActor.getSnapshot().context.credits).toBe(totalCredits)
|
||||||
|
await within(queryByTestId('billing-credits')).getByText(totalCredits)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Progress bar reflects ratio left of Free subscription', async () => {
|
||||||
|
const data = {
|
||||||
|
balance: {
|
||||||
|
monthlyApiCreditsRemaining: 10,
|
||||||
|
stableApiCreditsRemaining: 0,
|
||||||
|
},
|
||||||
|
subscriptions: {
|
||||||
|
monthlyPayAsYouGoApiCreditsTotal: 20,
|
||||||
|
name: 'free',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
server.use(
|
||||||
|
http.get('*/user/payment/balance', (req, res, ctx) => {
|
||||||
|
return HttpResponse.json(createUserPaymentBalanceResponse(data.balance))
|
||||||
|
}),
|
||||||
|
http.get('*/user/payment/subscriptions', (req, res, ctx) => {
|
||||||
|
return HttpResponse.json(
|
||||||
|
createUserPaymentSubscriptionsResponse(data.subscriptions)
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
http.get('*/org', (req, res, ctx) => {
|
||||||
|
return new HttpResponse(403)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const billingActor = createActor(billingMachine, {
|
||||||
|
input: BILLING_CONTEXT_DEFAULTS,
|
||||||
|
}).start()
|
||||||
|
|
||||||
|
const { queryByTestId } = render(
|
||||||
|
<BillingRemaining
|
||||||
|
mode={BillingRemainingMode.ProgressBarFixed}
|
||||||
|
billingActor={billingActor}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
await act(() => {
|
||||||
|
billingActor.send({
|
||||||
|
type: BillingTransition.Update,
|
||||||
|
apiToken: "it doesn't matter wtf this is :)",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalCredits =
|
||||||
|
data.balance.monthlyApiCreditsRemaining +
|
||||||
|
data.balance.stableApiCreditsRemaining
|
||||||
|
const monthlyCredits = data.subscriptions.monthlyPayAsYouGoApiCreditsTotal
|
||||||
|
const context = billingActor.getSnapshot().context
|
||||||
|
await expect(context.credits).toBe(totalCredits)
|
||||||
|
await expect(context.allowance).toBe(monthlyCredits)
|
||||||
|
|
||||||
|
await within(queryByTestId('billing-credits')).getByText(totalCredits)
|
||||||
|
await expect(
|
||||||
|
queryByTestId('billing-remaining-progress-bar-inner')
|
||||||
|
).toHaveStyle({
|
||||||
|
width: '50.00%',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
test('Shows infinite credits for Pro subscription', async () => {
|
||||||
|
const data = {
|
||||||
|
// These are all ignored
|
||||||
|
balance: {
|
||||||
|
monthlyApiCreditsRemaining: 10,
|
||||||
|
stableApiCreditsRemaining: 0,
|
||||||
|
},
|
||||||
|
subscriptions: {
|
||||||
|
// This should be ignored because it's Pro tier.
|
||||||
|
monthlyPayAsYouGoApiCreditsTotal: 20,
|
||||||
|
name: 'pro',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
server.use(
|
||||||
|
http.get('*/user/payment/balance', (req, res, ctx) => {
|
||||||
|
return HttpResponse.json(createUserPaymentBalanceResponse(data.balance))
|
||||||
|
}),
|
||||||
|
http.get('*/user/payment/subscriptions', (req, res, ctx) => {
|
||||||
|
return HttpResponse.json(
|
||||||
|
createUserPaymentSubscriptionsResponse(data.subscriptions)
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
http.get('*/org', (req, res, ctx) => {
|
||||||
|
return new HttpResponse(403)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const billingActor = createActor(billingMachine, {
|
||||||
|
input: BILLING_CONTEXT_DEFAULTS,
|
||||||
|
}).start()
|
||||||
|
|
||||||
|
const { queryByTestId } = render(
|
||||||
|
<BillingRemaining
|
||||||
|
mode={BillingRemainingMode.ProgressBarFixed}
|
||||||
|
billingActor={billingActor}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
await act(() => {
|
||||||
|
billingActor.send({
|
||||||
|
type: BillingTransition.Update,
|
||||||
|
apiToken: 'aosetuhsatuh',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(queryByTestId('infinity')).toBeVisible()
|
||||||
|
// You can't do `.not.toBeVisible` folks. When the query fails it's because
|
||||||
|
// no element could be found. toBeVisible should be used on an element
|
||||||
|
// that's found but may not be visible due to `display` or others.
|
||||||
|
await expect(queryByTestId('billing-remaining-progress-bar-inline')).toBe(
|
||||||
|
null
|
||||||
|
)
|
||||||
|
})
|
||||||
|
test('Shows infinite credits for Enterprise subscription', async () => {
|
||||||
|
const data = {
|
||||||
|
// These are all ignored, user is part of an org.
|
||||||
|
balance: {
|
||||||
|
monthlyApiCreditsRemaining: 10,
|
||||||
|
stableApiCreditsRemaining: 0,
|
||||||
|
},
|
||||||
|
subscriptions: {
|
||||||
|
// This should be ignored because it's Pro tier.
|
||||||
|
monthlyPayAsYouGoApiCreditsTotal: 20,
|
||||||
|
// This should be ignored because the user is part of an Org.
|
||||||
|
name: 'free',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
server.use(
|
||||||
|
http.get('*/user/payment/balance', (req, res, ctx) => {
|
||||||
|
return HttpResponse.json(createUserPaymentBalanceResponse(data.balance))
|
||||||
|
}),
|
||||||
|
http.get('*/user/payment/subscriptions', (req, res, ctx) => {
|
||||||
|
return HttpResponse.json(
|
||||||
|
createUserPaymentSubscriptionsResponse(data.subscriptions)
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
// Ok finally the first use of an org lol
|
||||||
|
http.get('*/org', (req, res, ctx) => {
|
||||||
|
return HttpResponse.json(createOrgResponse())
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const billingActor = createActor(billingMachine, {
|
||||||
|
input: BILLING_CONTEXT_DEFAULTS,
|
||||||
|
}).start()
|
||||||
|
|
||||||
|
const { queryByTestId } = render(
|
||||||
|
<BillingRemaining
|
||||||
|
mode={BillingRemainingMode.ProgressBarFixed}
|
||||||
|
billingActor={billingActor}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
await act(() => {
|
||||||
|
billingActor.send({
|
||||||
|
type: BillingTransition.Update,
|
||||||
|
apiToken: 'aosetuhsatuh',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// The result should be the same as Pro users.
|
||||||
|
await expect(queryByTestId('infinity')).toBeVisible()
|
||||||
|
await expect(queryByTestId('billing-remaining-progress-bar-inline')).toBe(
|
||||||
|
null
|
||||||
|
)
|
||||||
|
})
|
Reference in New Issue
Block a user