Pārlūkot izejas kodu

Add browser testing

Use Cypress to process server-side navigations.
master
TheoryOfNekomata pirms 7 mēnešiem
vecāks
revīzija
57ff8413df
13 mainītis faili ar 938 papildinājumiem un 61 dzēšanām
  1. +2
    -2
      packages/iceform-next-sandbox/src/handlers/note.ts
  2. +25
    -9
      packages/iceform-next-sandbox/src/pages/notes/[noteId].tsx
  3. +6
    -1
      packages/iceform-next-sandbox/src/pages/notes/index.tsx
  4. +26
    -0
      packages/iceform-next/cypress.config.ts
  5. +93
    -0
      packages/iceform-next/cypress/e2e/form.cy.ts
  6. Binārs
      packages/iceform-next/cypress/fixtures/file.jpg
  7. +37
    -0
      packages/iceform-next/cypress/support/commands.ts
  8. +20
    -0
      packages/iceform-next/cypress/support/e2e.ts
  9. +4
    -1
      packages/iceform-next/package.json
  10. +2
    -2
      packages/iceform-next/src/client/components/Form.tsx
  11. +7
    -1
      packages/iceform-next/src/server/action/gssp.ts
  12. +0
    -17
      packages/iceform-next/test/client.test.tsx
  13. +716
    -28
      pnpm-lock.yaml

+ 2
- 2
packages/iceform-next-sandbox/src/handlers/note.ts Parādīt failu

@@ -203,7 +203,7 @@ const createNote = (params: NoteCollectionParams): NextApiHandler => async (req,
id: newId,
title,
content,
image: `data:${image.type};base64,${image.toString('base64')}`,
image: image ? `data:${image.type};base64,${image.toString('base64')}` : null,
})}\n`,
{
flag: 'a',
@@ -225,7 +225,7 @@ const createNote = (params: NoteCollectionParams): NextApiHandler => async (req,
id: newId,
title,
content,
image: `data:${image.type};base64,${image.toString('base64')}`,
image: image ? `data:${image.type};base64,${image.toString('base64')}` : null,
});
};



+ 25
- 9
packages/iceform-next-sandbox/src/pages/notes/[noteId].tsx Parādīt failu

@@ -11,13 +11,15 @@ export interface NotesItemPageProps {
title: string;
content: string;
image: string;
}
},
noscript?: boolean,
}

const NotesItemPage: Iceform.NextPage<NotesItemPageProps> = ({
req,
res,
note,
noscript,
}) => {
const router = useRouter();
const body = (res.body ?? note ?? {}) as Record<string, unknown>;
@@ -33,7 +35,11 @@ const NotesItemPage: Iceform.NextPage<NotesItemPageProps> = ({
const [responseData, setResponseData] = React.useState<unknown>();
React.useEffect(() => {
// response.bodyUsed might be undefined, so we use a strict comparison
if (response?.bodyUsed === false && response.status !== 204) {
if (
response?.bodyUsed === false
&& response.status !== 204
&& response.headers.get('Content-Type')?.startsWith('application/json')
) {
response?.json().then((responseData) => {
setResponseData(responseData);
});
@@ -52,6 +58,7 @@ const NotesItemPage: Iceform.NextPage<NotesItemPageProps> = ({
clientMethod="put"
refresh={defaultRefresh}
aria-label="Edit Existing Note"
disableFetch={noscript}
>
<fieldset
className="contents"
@@ -72,10 +79,16 @@ const NotesItemPage: Iceform.NextPage<NotesItemPageProps> = ({
/>
</div>
<div>
<img
src={body.image as string}
alt={body.title as string}
/>
{
typeof body.image === 'string'
&& body.image.length > 0
&& (
<img
src={body.image}
alt={body.title as string}
/>
)
}
<label>
<span className="after:block">Image</span>
<input type="file" name="image" />
@@ -91,10 +104,10 @@ const NotesItemPage: Iceform.NextPage<NotesItemPageProps> = ({
</div>
<div className="flex justify-end gap-4">
<div>
<ActionButton type="submit" className="bg-white text-black">Update</ActionButton>
<ActionButton type="submit" form="delete-note-form">Delete</ActionButton>
</div>
<div>
<ActionButton type="submit" form="delete-note-form">Delete</ActionButton>
<ActionButton type="submit" className="bg-white text-black">Update</ActionButton>
</div>
</div>
</div>
@@ -112,6 +125,8 @@ const NotesItemPage: Iceform.NextPage<NotesItemPageProps> = ({
await router.push('/notes');
}}
id="delete-note-form"
aria-label="Delete Note"
disableFetch={noscript}
/>
</div>
</div>
@@ -121,7 +136,7 @@ const NotesItemPage: Iceform.NextPage<NotesItemPageProps> = ({
// TODO type safety
export const getServerSideProps = Iceform.destination.getServerSideProps({
fn: async (actionReq, actionRes, ctx) => {
const {noteId} = ctx.query;
const {noteId, noscript} = ctx.query;
let origin: string;
if (ctx.req.headers.referer) {
const refererUrl = new URL(ctx.req.headers.referer as string);
@@ -142,6 +157,7 @@ export const getServerSideProps = Iceform.destination.getServerSideProps({
return {
props: {
note,
noscript: noscript === 'true',
},
};
}


+ 6
- 1
packages/iceform-next-sandbox/src/pages/notes/index.tsx Parādīt failu

@@ -13,10 +13,12 @@ export interface NotesPageProps {
content: string;
image: string;
}[];
noscript?: boolean;
}

const NotesPage: NextPage<NotesPageProps> = ({
notes,
noscript,
}) => {
const router = useRouter();

@@ -25,7 +27,7 @@ const NotesPage: NextPage<NotesPageProps> = ({
<div className="px-8 max-w-screen-sm mx-auto">
<Iceform.Form
method="post"
action="/a/notes"
action={noscript ? '/a/notes?noscript=true' : '/a/notes'}
clientAction="/api/notes"
refresh={async (response) => {
if (response.status !== 201) {
@@ -36,6 +38,7 @@ const NotesPage: NextPage<NotesPageProps> = ({
}}
aria-label="Create New Note"
className="contents"
disableFetch={noscript}
>
<fieldset
className="contents"
@@ -100,6 +103,7 @@ const NotesPage: NextPage<NotesPageProps> = ({
refresh={async () => {
await router.push('/notes');
}}
disableFetch={noscript}
>
<div>
<ActionButton type="submit">
@@ -156,6 +160,7 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
return {
props: {
notes,
noscript: ctx.query?.noscript === 'true',
},
};
}


+ 26
- 0
packages/iceform-next/cypress.config.ts Parādīt failu

@@ -0,0 +1,26 @@
import { defineConfig } from "cypress";
import { resolve } from 'path';
import { mkdir, rm, writeFile } from 'fs/promises';

export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
on('task', {
async resetDb() {
console.log(resolve('../iceform-next-sandbox/.db'));
await rm('../iceform-next-sandbox/.db', { force: true, recursive: true });
await mkdir('../iceform-next-sandbox/.db');
await writeFile('../iceform-next-sandbox/.db/notes.jsonl', '');
return null;
}
});
},
},
component: {
devServer: {
framework: 'next',
bundler: 'webpack',
},
},
});

+ 93
- 0
packages/iceform-next/cypress/e2e/form.cy.ts Parādīt failu

@@ -0,0 +1,93 @@
describe('form', () => {
beforeEach(() => {
cy.task('resetDb');
});

describe('script', () => {
it('submits form with complete data', () => {
cy.visit('http://localhost:3000/notes');
cy.intercept('POST', '/api/notes').as('action');
cy.get('form').should('exist').within(() => {
cy.get('input[name="title"]').type('test title');
cy.get('input[name="image"]').selectFile('cypress/fixtures/file.jpg');
cy.get('textarea[name="content"]').type('test content');
cy.get('button[type="submit"]').click();
cy.wait('@action').its('response.statusCode').should('eq', 201);
});

cy.url().should('match', /\/notes\/[a-z0-9-]+$/);
});

it('submits form with incomplete data', () => {
cy.visit('http://localhost:3000/notes');
cy.intercept('POST', '/api/notes').as('action');
cy.get('form').should('exist').within(() => {
cy.get('input[name="title"]').type('test title');
cy.get('textarea[name="content"]').type('test content');
cy.get('button[type="submit"]').click();
cy.wait('@action').its('response.statusCode').should('eq', 201);
});

cy.url().should('match', /\/notes\/[a-z0-9-]+$/);
});

it('handles redirects', () => {
cy.visit('http://localhost:3000/notes');
cy.intercept('DELETE', '/api/notes/*').as('action');
cy.get('form').should('exist').within(() => {
cy.get('input[name="title"]').type('test title');
cy.get('textarea[name="content"]').type('test content');
cy.get('button[type="submit"]').click();
});
cy.get('button').contains('Delete').click();
cy.wait('@action').its('response.statusCode').should('eq', 204);
cy.url().should('match', /\/notes$/);
});
});

describe('noscript', () => {
it('submits form with complete data', () => {
cy.visit('http://localhost:3000/notes?noscript=true');
cy.intercept('POST', '/a/notes?noscript=true').as('action');
cy.intercept('POST', '/notes/*').as('destination');
cy.get('form').should('exist').within(() => {
cy.get('input[name="title"]').type('test title');
cy.get('input[name="image"]').selectFile('cypress/fixtures/file.jpg');
cy.get('textarea[name="content"]').type('test content');
cy.get('button[type="submit"]').click();
});
cy.wait('@action').its('response.statusCode').should('eq', 307);
cy.wait('@destination').its('response.statusCode').should('eq', 201);
cy.url().should('match', /\/notes\/[a-z0-9-]+\?/);
});

it('submits form with incomplete data', () => {
cy.visit('http://localhost:3000/notes?noscript=true');
cy.intercept('POST', '/a/notes?noscript=true').as('action');
cy.intercept('POST', '/notes/*').as('destination');
cy.get('form').should('exist').within(() => {
cy.get('input[name="title"]').type('test title');
cy.get('textarea[name="content"]').type('test content');
cy.get('button[type="submit"]').click();
});
cy.wait('@action').its('response.statusCode').should('eq', 307);
cy.wait('@destination').its('response.statusCode').should('eq', 201);
cy.url().should('match', /\/notes\/[a-z0-9-]+\?/);
});

it('handles redirects', () => {
cy.visit('http://localhost:3000/notes?noscript=true');
cy.get('form').should('exist').within(() => {
cy.get('input[name="title"]').type('test title');
cy.get('textarea[name="content"]').type('test content');
cy.get('button[type="submit"]').click();
});
cy.intercept('POST', '/a/notes/*').as('action');
cy.intercept('POST', '/notes').as('destination');
cy.get('button').contains('Delete').click();
cy.wait('@action').its('response.statusCode').should('eq', 307);
cy.wait('@destination').its('response.statusCode').should('eq', 200);
cy.url().should('match', /\/notes$/);
});
});
});

Binārs
packages/iceform-next/cypress/fixtures/file.jpg Parādīt failu

Pirms Pēc
Platums: 673  |  Augstums: 721  |  Izmērs: 87 KiB

+ 37
- 0
packages/iceform-next/cypress/support/commands.ts Parādīt failu

@@ -0,0 +1,37 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }

+ 20
- 0
packages/iceform-next/cypress/support/e2e.ts Parādīt failu

@@ -0,0 +1,20 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************

// Import commands.js using ES2015 syntax:
import './commands'

// Alternatively you can use CommonJS syntax:
// require('./commands')

+ 4
- 1
packages/iceform-next/package.json Parādīt failu

@@ -23,6 +23,7 @@
"@types/react": "^18.0.27",
"@types/testing-library__jest-dom": "^5.14.9",
"@vitest/coverage-v8": "^0.33.0",
"cypress": "^13.3.0",
"eslint": "^8.35.0",
"eslint-config-lxsmnsyc": "^0.5.0",
"express": "^4.18.2",
@@ -50,7 +51,9 @@
"watch": "pridepack watch",
"start": "pridepack start",
"dev": "pridepack dev",
"test": "vitest"
"test:jsdom": "vitest",
"test:dom": "cypress run",
"cypress:config": "cypress open"
},
"private": false,
"description": "Simple isomorphic forms for Next.",


+ 2
- 2
packages/iceform-next/src/client/components/Form.tsx Parādīt failu

@@ -70,8 +70,8 @@ export const Form = React.forwardRef<FormDerivedElement, FormProps>(({

React.useEffect(() => {
// hide server override in client
setServerMethodOverride(false);
}, []);
setServerMethodOverride(disableFetch);
}, [disableFetch]);

// TODO csrf token



+ 7
- 1
packages/iceform-next/src/server/action/gssp.ts Parādīt failu

@@ -125,7 +125,13 @@ export const getServerSideProps = (options: ActionWrapperOptions): GetServerSide
: referer;

const url = new URL(redirectDestinationRaw, 'http://example.com');
url.searchParams.set(options.requestIdFormKey ?? REQUEST_ID_FORM_KEY, requestId);
const redirectSearchParams = Object.fromEntries(new URLSearchParams(url.search).entries());
const searchParams = new URLSearchParams({
...((ctx.query ?? {}) as Record<string, string>),
...redirectSearchParams,
});
searchParams.set(options.requestIdFormKey ?? REQUEST_ID_FORM_KEY, requestId);
url.search = searchParams.toString();

return {
redirect: {


+ 0
- 17
packages/iceform-next/test/client.test.tsx Parādīt failu

@@ -41,21 +41,4 @@ describe('Form', () => {

expect(onSubmit).toHaveBeenCalled();
});

it.skip('calls the submit handler on submit with a custom component', async () => {
// TODO
const onSubmit = vi.fn();

render(
<Form onSubmit={onSubmit} aria-label="Form">
<button type="submit">Submit</button>
</Form>,
);

const button = screen.getByRole('button', { name: 'Submit' });

await userEvent.click(button);

expect(onSubmit).toHaveBeenCalled();
});
});

+ 716
- 28
pnpm-lock.yaml
Failā izmaiņas netiks attēlotas, jo tās ir par lielu
Parādīt failu


Notiek ielāde…
Atcelt
Saglabāt