-
Notifications
You must be signed in to change notification settings - Fork 406
chore(e2e): Tests for validating billing hooks behaviour #7161
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,36 @@ | ||||||||||||||||||||||||||||||
| 'use client'; | ||||||||||||||||||||||||||||||
| import { usePlans, useSubscription, useStatements } from '@clerk/nextjs/experimental'; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| export default function Home() { | ||||||||||||||||||||||||||||||
| const { data: plans, count: planCount } = usePlans(); | ||||||||||||||||||||||||||||||
| const { data: subscription } = useSubscription(); | ||||||||||||||||||||||||||||||
| const { data: statements, count: statementCount } = useStatements(); | ||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||
| <main> | ||||||||||||||||||||||||||||||
| {plans?.map(plan => ( | ||||||||||||||||||||||||||||||
| <div key={plan.id}> | ||||||||||||||||||||||||||||||
| <h2>Plan: {plan.name}</h2> | ||||||||||||||||||||||||||||||
| <p>{plan.description}</p> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| {planCount > 0 ? <p>Plans found</p> : <p>No plans found</p>} | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| {statements?.map(statement => ( | ||||||||||||||||||||||||||||||
| <div key={statement.id}> | ||||||||||||||||||||||||||||||
| <p>Statement total: {statement.totals.grandTotal.amountFormatted}</p> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| {statementCount > 0 ? <p>Statements found</p> : <p>No statements found</p>} | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| {subscription ? ( | ||||||||||||||||||||||||||||||
| <div> | ||||||||||||||||||||||||||||||
| <h2>Subscribed to {subscription.subscriptionItems[0].plan.name}</h2> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||
| <p>No subscription found</p> | ||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||
|
Comment on lines
+27
to
+33
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unsafe array access will cause runtime error. Line 29 directly accesses
This will throw a runtime error if Apply this diff to add proper bounds checking: {subscription ? (
<div>
- <h2>Subscribed to {subscription.subscriptionItems[0].plan.name}</h2>
+ <h2>Subscribed to {subscription.subscriptionItems?.[0]?.plan?.name || 'Unknown Plan'}</h2>
</div>
) : (📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| </main> | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| import { expect, test } from '@playwright/test'; | ||
|
|
||
| import { appConfigs } from '../presets'; | ||
| import type { FakeUser } from '../testUtils'; | ||
| import { createTestUtils, testAgainstRunningApps } from '../testUtils'; | ||
|
|
||
| testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('billing hooks @billing', ({ app }) => { | ||
| test.describe.configure({ mode: 'parallel' }); | ||
| test.skip(!app.name.includes('next'), 'Skipping: Only runs on next'); | ||
|
|
||
| let fakeUser: FakeUser; | ||
|
|
||
| test.beforeAll(async () => { | ||
| const u = createTestUtils({ app }); | ||
| fakeUser = u.services.users.createFakeUser(); | ||
| await u.services.users.createBapiUser(fakeUser); | ||
| }); | ||
|
|
||
| test.afterAll(async () => { | ||
| await fakeUser.deleteIfExists(); | ||
| await app.teardown(); | ||
| }); | ||
|
|
||
| test.describe('when signed out', () => { | ||
| test('renders billing hooks with plans, but no statements and no subscription', async ({ page, context }) => { | ||
| const u = createTestUtils({ app, page, context }); | ||
| await u.po.page.goToRelative('/billing/hooks'); | ||
|
|
||
| await u.po.page.waitForClerkJsLoaded(); | ||
|
|
||
| await expect(u.po.page.getByText('Plans found')).toBeVisible(); | ||
| await expect(u.po.page.getByRole('heading', { name: 'Plan: Pro' })).toBeVisible(); | ||
| await expect(u.po.page.getByText('No statements found')).toBeVisible(); | ||
| await expect(u.po.page.getByText('No subscription found')).toBeVisible(); | ||
| }); | ||
| }); | ||
|
|
||
| test.describe('when signed in', () => { | ||
| test.describe.configure({ mode: 'serial' }); | ||
| test('subscribes to a plan', async ({ page, context }) => { | ||
| const u = createTestUtils({ app, page, context }); | ||
| await u.po.signIn.goTo(); | ||
| await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password }); | ||
| await u.po.page.goToRelative('/pricing-table?newSubscriptionRedirectUrl=/pricing-table'); | ||
| await u.po.pricingTable.waitForMounted(); | ||
| await u.po.pricingTable.startCheckout({ planSlug: 'plus' }); | ||
| await u.po.checkout.waitForMounted(); | ||
| await u.po.checkout.fillTestCard(); | ||
| await u.po.checkout.clickPayOrSubscribe(); | ||
| await expect(u.po.checkout.root.getByText('Payment was successful!')).toBeVisible(); | ||
| await u.po.checkout.confirmAndContinue(); | ||
| }); | ||
|
|
||
| test('renders billing hooks with plans, statements and subscription', async ({ page, context }) => { | ||
| const u = createTestUtils({ app, page, context }); | ||
| await u.po.signIn.goTo(); | ||
| await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password }); | ||
| await u.po.page.goToRelative('/billing/hooks'); | ||
|
|
||
| await u.po.page.waitForClerkJsLoaded(); | ||
|
|
||
| await expect(u.po.page.getByText('Plans found')).toBeVisible(); | ||
| await expect(u.po.page.getByRole('heading', { name: 'Plan: Pro' })).toBeVisible(); | ||
|
|
||
| await expect(u.po.page.getByText('Statements found')).toBeVisible(); | ||
| await expect(u.po.page.getByText('Statement total: 99.96')).toBeVisible(); | ||
|
|
||
| await expect(u.po.page.getByRole('heading', { name: 'Subscribed to Plus' })).toBeVisible(); | ||
|
|
||
| await u.page.evaluate(async () => { | ||
| await window.Clerk.signOut({ | ||
| redirectUrl: '/billing/hooks', | ||
| }); | ||
| }); | ||
|
|
||
| await expect(u.po.page.getByText('Plans found')).toBeVisible(); | ||
| await expect(u.po.page.getByRole('heading', { name: 'Plan: Pro' })).toBeVisible(); | ||
| await expect(u.po.page.getByText('No statements found')).toBeVisible(); | ||
| await expect(u.po.page.getByText('No subscription found')).toBeVisible(); | ||
|
|
||
| await expect(u.po.page.getByRole('heading', { name: 'Subscribed to Plus' })).toBeHidden(); | ||
| await expect(u.po.page.getByText('Statements found', { exact: true })).toBeHidden(); | ||
| await expect(u.po.page.getByText('Statement total: 99.96', { exact: true })).toBeHidden(); | ||
| }); | ||
|
Comment on lines
+54
to
+84
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add wait after sign-out to prevent race condition. After calling
This creates a race condition where assertions may run before sign-out completes. Apply this diff to add an explicit wait: await u.page.evaluate(async () => {
await window.Clerk.signOut({
redirectUrl: '/billing/hooks',
});
});
+
+ // Wait for sign-out to complete and page to reload
+ await u.po.page.waitForURL('**/billing/hooks');
+ await u.po.page.waitForClerkJsLoaded();
+ // Wait for signed-out state to render
+ await expect(u.po.page.getByText('No subscription found')).toBeVisible({ timeout: 5000 });
await expect(u.po.page.getByText('Plans found')).toBeVisible(); |
||
| }); | ||
| }); | ||
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.
🧩 Analysis chain
Add explicit return type and handle loading/error states.
The component definition has several concerns:
isLoadinganderrorproperties that should be handled to improve UX.Apply this diff to add return type and basic state handling:
Note: Verify that the experimental hooks actually expose
isLoadinganderrorproperties before applying this change.🏁 Script executed:
Length of output: 9370
🏁 Script executed:
Length of output: 2054
🏁 Script executed:
Length of output: 870
🏁 Script executed:
Length of output: 27
🏁 Script executed:
Length of output: 2331
🏁 Script executed:
Length of output: 1027
🏁 Script executed:
Length of output: 30
Add explicit return type and handle loading/error states from hooks.
The component is missing two improvements:
JSX.Element.isLoadinganderrorproperties that should be handled for better UX. The component currently renders potentially undefined data during loading and ignores errors.The hooks expose these properties via their return types:
usePlans()anduseStatements()returnPaginatedResources<T>witherror,isLoading,isFetchinguseSubscription()returnsSubscriptionResultwitherror,isLoading,isFetching🤖 Prompt for AI Agents