Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions examples/twd-test-app/src/components/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import React from 'react';
import { Link } from 'react-router';

type PaginationProps = {
currentPage: number;
totalPages: number;
getPageHref: (page: number) => string;
};

const buttonBaseStyle: React.CSSProperties = {
minWidth: '38px',
height: '38px',
padding: '0 12px',
borderRadius: '10px',
border: '1px solid #cbd5e1',
backgroundColor: '#ffffff',
color: '#334155',
fontSize: '14px',
fontWeight: 600,
cursor: 'pointer',
};

const Pagination: React.FC<PaginationProps> = ({
currentPage,
totalPages,
getPageHref,
}) => {
if (totalPages <= 1) {
return null;
}

const pages = Array.from({ length: totalPages }, (_, index) => index + 1);

return (
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: '16px',
marginTop: '22px',
flexWrap: 'wrap',
}}
>
<p style={{ margin: 0, fontSize: '14px', color: '#64748b' }}>
Pagina {currentPage} de {totalPages}
</p>

<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
<Link
to={getPageHref(Math.max(currentPage - 1, 1))}
aria-disabled={currentPage === 1}
style={{
...buttonBaseStyle,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
textDecoration: 'none',
opacity: currentPage === 1 ? 0.5 : 1,
cursor: currentPage === 1 ? 'not-allowed' : 'pointer',
pointerEvents: currentPage === 1 ? 'none' : 'auto',
}}
>
Anterior
</Link>

{pages.map((page) => (
<Link
key={page}
to={getPageHref(page)}
style={{
...buttonBaseStyle,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
textDecoration: 'none',
backgroundColor: currentPage === page ? '#2563eb' : '#ffffff',
borderColor: currentPage === page ? '#2563eb' : '#cbd5e1',
color: currentPage === page ? '#ffffff' : '#334155',
}}
>
{page}
</Link>
))}

<Link
to={getPageHref(Math.min(currentPage + 1, totalPages))}
aria-disabled={currentPage === totalPages}
style={{
...buttonBaseStyle,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
textDecoration: 'none',
opacity: currentPage === totalPages ? 0.5 : 1,
cursor: currentPage === totalPages ? 'not-allowed' : 'pointer',
pointerEvents: currentPage === totalPages ? 'none' : 'auto',
}}
>
Siguiente
</Link>
</div>
</div>
);
};

export default Pagination;
31 changes: 31 additions & 0 deletions examples/twd-test-app/src/loaders/contactsLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { LoaderFunctionArgs } from "react-router";

export type Contact = {
id: number;
name: string;
email: string;
};

type ContactResponse = {
contacts: Contact[];
currentPage: number;
totalPages: number;
}

export const contactsLoader = async ({ request }: LoaderFunctionArgs) => {
const url = new URL(request.url);
const pageParam = url.searchParams.get("page");
console.log(pageParam)
const response = await fetch("http://localhost:3000/v1/contacts");

if (!response.ok) {
throw new Response("Failed to load contacts", {
status: response.status,
statusText: response.statusText,
});
}

const contactsResponse = (await response.json()) as ContactResponse;

return contactsResponse;
};
138 changes: 138 additions & 0 deletions examples/twd-test-app/src/pages/Contacts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import React from 'react';
import { useLoaderData, } from 'react-router';
import type { contactsLoader } from '../loaders/contactsLoader';
import Pagination from '../components/Pagination';

const Contacts: React.FC = () => {
const { contacts, currentPage, totalPages } = useLoaderData<typeof contactsLoader>();


const getPageHref = (page: number) => `/contacts?page=${page}`;

return (
<div
style={{
minHeight: '100vh',
padding: '32px 20px',
background: 'linear-gradient(180deg, #f8fafc 0%, #e2e8f0 100%)',
}}
>
<div
style={{
maxWidth: '720px',
margin: '0 auto',
backgroundColor: '#ffffff',
borderRadius: '16px',
padding: '28px',
boxShadow: '0 18px 45px rgba(15, 23, 42, 0.10)',
border: '1px solid #e2e8f0',
}}
>
<div style={{ marginBottom: '24px' }}>
<p
style={{
margin: 0,
fontSize: '12px',
fontWeight: 700,
letterSpacing: '0.08em',
textTransform: 'uppercase',
color: '#2563eb',
}}
>
Agenda mock
</p>
<h1
style={{
margin: '8px 0 6px',
fontSize: '32px',
color: '#0f172a',
}}
>
Contactos
</h1>
<p style={{ margin: 0, color: '#475569', fontSize: '15px' }}>
Una lista simple de contactos con nombre y correo.
</p>
</div>

<div style={{ display: 'grid', gap: '14px' }}>
{contacts.length === 0 ? (
<div
style={{
padding: '22px',
borderRadius: '12px',
backgroundColor: '#f8fafc',
border: '1px dashed #cbd5e1',
color: '#64748b',
textAlign: 'center',
}}
>
No hay contactos disponibles.
</div>
) : (
contacts.map((contact) => (
<div
key={contact.id}
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: '16px',
padding: '16px 18px',
borderRadius: '12px',
backgroundColor: '#f8fafc',
border: '1px solid #e2e8f0',
}}
>
<div>
<p
style={{
margin: 0,
fontSize: '17px',
fontWeight: 600,
color: '#0f172a',
}}
>
{contact.name}
</p>
<p
style={{
margin: '6px 0 0',
fontSize: '14px',
color: '#64748b',
}}
>
{contact.email}
</p>
</div>

<span
style={{
padding: '6px 10px',
borderRadius: '999px',
backgroundColor: '#dbeafe',
color: '#1d4ed8',
fontSize: '12px',
fontWeight: 700,
}}
>
Contacto
</span>
</div>
))
)}
</div>



<Pagination
currentPage={currentPage}
totalPages={totalPages}
getPageHref={getPageHref}
/>
</div>
</div>
);
};

export default Contacts;
7 changes: 7 additions & 0 deletions examples/twd-test-app/src/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,19 @@ import MockComponent from "./pages/MockComponent";
import Responsive from "./pages/Responsive";
import BlurValidation from "./pages/BlurValidation";
import ComboboxSelect from "./pages/ComboboxSelect";
import Contacts from "./pages/Contacts";
import { contactsLoader } from "./loaders/contactsLoader";

const router = createBrowserRouter([
{
path: "/",
Component: App,
},
{
path: "/contacts",
loader: contactsLoader,
Component: Contacts,
},
{
path: "/assertions",
Component: Assertions,
Expand Down
48 changes: 48 additions & 0 deletions examples/twd-test-app/src/twd-tests/contacts.twd.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { twd, expect, screenDom, userEvent } from "../../../../src";
import { describe, it, beforeEach } from "../../../../src/runner";

const contactsResponse = [
{ id: 1, name: "Ana Garcia", email: "ana.garcia@example.com" },
{ id: 2, name: "Luis Martinez", email: "luis.martinez@example.com" },
{ id: 3, name: "Carmen Lopez", email: "carmen.lopez@example.com" },
{ id: 4, name: "Javier Romero", email: "javier.romero@example.com" },
];

describe("contactsPage", () => {
beforeEach(() => {
twd.clearRequestMockRules();
});

it("renders the contacts list from the loader response", async () => {
await twd.mockRequest("contactsList", {
method: "GET",
response: {
contacts: contactsResponse,
currentPage: 2,
totalPages: 2,
},
url: "/v1/contacts",
});

await twd.visit("/contacts");
await twd.waitForRequest("contactsList");

const heading = screenDom.getByRole("heading", { name: /contactos/i });
twd.should(heading, "be.visible");

for (const contact of contactsResponse.slice(0, 2)) {
const name = screenDom.getByText(contact.name);
const email = screenDom.getByText(contact.email);

twd.should(name, "be.visible");
twd.should(email, "be.visible");
}

const pageTwoLink = screenDom.getByRole("link", { name: "2" });
await userEvent.click(pageTwoLink);

// TODO: there is a bug with twd assertion
// await twd.url().should("contain.url", "/contacts?page=2");

});
});
Loading