Next.js 26 ▲ Testing Strategies — Playwright Instant Helpers & Unit Testing
Testing a Next.js application has always required a split personality. Unit tests verify that a component renders the right markup given the right props, that a utility function handles edge cases, that a hook returns the expected state. End-to-end tests verify that a user can actually navigate from page A to page B, that a form submission reaches a Server Action and the UI updates, that a session cookie is respected across routes. The two layers answer different questions, and neither replaces the other. What changed in recent Next.js releases is the precision of the E2E layer. With the App Router’s streaming and partial prerendering, “the page loaded” is no longer a binary state. A route can render its shell instantly while dynamic data streams in seconds later. The instant() helper from @next/playwright exists to make that distinction testable .
The unit testing side has its own constraint that is easy to miss. Vitest and React Testing Library work well for synchronous Server Components and Client Components, but they do not support async Server Components—the kind that fetch data during render . This is not a temporary gap that a configuration change fixes; it is a consequence of how async components interact with the test renderer. The official recommendation is to use E2E tests for async components and reserve unit tests for the synchronous parts of the tree . Understanding where each tool fits prevents the frustration of trying to force Vitest to do something it was not designed to do.
Key point: Playwright’s instant() helper tests whether a route’s shell renders before dynamic data arrives. Unit tests with Vitest test everything except async Server Components. Knowing which layer tests which behavior is the difference between a test suite that catches regressions and one that merely passes.
Why testing strategies change with the App Router
The streaming shell problem. In a traditional SPA, a navigation either renders the new page or it does not. There is no intermediate state. In the App Router with Cache Components and Suspense, a navigation can commit the cached shell immediately while request-time data streams in afterward. A test that waits for the full page to load verifies that the page eventually renders, but it says nothing about whether the user saw something useful immediately. The instant() helper exists because this distinction matters for perceived performance, and it cannot be tested by waiting for the final state .
The async Server Component limitation. Vitest renders components through React Testing Library, which uses a synchronous render path adapted for testing. An async Server Component returns a Promise, and the test renderer has no way to await it within the synchronous render() call. The Next.js documentation states this plainly: “Vitest currently does not support them” . This is not a missing feature that will arrive in a future release; it is a boundary of the testing model. The workaround is architectural: extract data fetching into a service or DAL that can be tested independently, and test the component’s rendering through E2E .
The cookie-based testing API. The instant() helper works by setting a cookie that tells Next.js to serve only cached data during navigations. While the cookie is active, the router renders only what is in the prefetch cache, and dynamic data is deferred until the callback returns . This design is intentionally thin—a single cookie—so that other tools can replicate the behavior. It also means the helper requires a warm cache to be meaningful: it assumes prefetches have completed, so it tests structure, not network timing .
The trade-off. Playwright E2E tests are slower than unit tests. They spin up a browser, navigate, and wait for selectors. Running them on every commit is expensive. But for the behaviors that only emerge in a real browser with real streaming—instant shells, Suspense fallbacks, Server Action mutations—there is no substitute. The testing strategy that works is not “more of one, less of the other.” It is knowing which questions belong to which layer.
a. Setting up Vitest for unit testing
Vitest is the recommended unit testing framework for Next.js. It integrates with React Testing Library and provides a fast, watch-mode development experience . The setup is minimal: install dependencies, create a config file, and add a test script.
// vitest.config.mts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
// tsconfigPaths resolves the @/ imports used in Next.js projects.
// react() enables JSX transformation for component tests.
plugins: [tsconfigPaths(), react()],
test: {
// jsdom provides a browser-like environment for rendering components.
environment: 'jsdom',
},
});
// package.json — add the test script
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"test": "vitest"
}
}
The first test verifies that a page component renders its heading. This is the simplest possible test, and it establishes the pattern: import the component, render it, query the output.
// __tests__/page.test.tsx
import { expect, test } from 'vitest';
import { render, screen } from '@testing-library/react';
import Page from '../app/page';
test('Page renders the home heading', () => {
// render() mounts the component in the jsdom environment.
render(<Page />);
// getByRole queries by accessibility role, not CSS class.
// This makes the test resilient to styling changes.
expect(
screen.getByRole('heading', { level: 1, name: 'Home' })
).toBeDefined();
});
The limitation appears when the page component is async. If Page fetches data during render, the render() call receives a Promise, and the test fails before assertions run . The solution is not to mock the fetch and force the async component through a synchronous renderer. The solution is to test the fetching logic separately and test the component’s output through Playwright.
b. Testing with Playwright and the instant helper
Playwright drives a real browser through Chromium, Firefox, and WebKit. It is the tool for verifying behaviors that depend on the full Next.js runtime: streaming, prefetching, client-side navigation, and Server Action responses . The instant() helper adds a capability specific to Cache Components: asserting what is visible before dynamic data arrives.
// e2e/navigation.spec.ts
import { test, expect } from '@playwright/test';
import { instant } from '@next/playwright';
test('dashboard shell renders before data streams in', async ({ page }) => {
// Start from the home page, where the dashboard link is prefetched.
await page.goto('/');
// instant() holds back request-time server content.
// Inside the callback, only cached and prefetched content renders.
await instant(page, async () => {
await page.click('a[href="/dashboard"]');
// The loading shell is visible because dynamic data is deferred.
await expect(page.locator('[data-testid="loading"]')).toBeVisible();
});
// After instant() returns, the cookie is cleared.
// Dynamic data streams in normally, and the final state is reachable.
await expect(page.locator('[data-testid="content"]')).toBeVisible();
});
The mechanics of instant() are deliberately simple. It sets a cookie named next-instant-navigation-testing, which tells the Next.js router to serve only cached data during the callback. When the callback completes, the cookie is cleared and normal behavior resumes . The helper assumes a warm cache: all prefetches have completed and cacheable data is available. This means the test verifies whether the route is structured correctly for instant navigation, not whether the network happened to be fast enough . If content you expected to be cached is missing inside the callback, the cause is a missing use cache directive, a misplaced Suspense boundary, or a similar structural gap.
The helper is experimental and requires Cache Components to be enabled. In production builds, it is disabled unless you explicitly opt in through experimental.exposeTestingApiInProductionBuild in next.config.js . This opt-in exists because the testing API should never be exposed on a live production site—only in preview deployments or CI environments.
Complete Example Session
// ============================================
// PART 1: VITEST CONFIGURATION
// ============================================
// vitest.config.mts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [tsconfigPaths(), react()],
test: {
environment: 'jsdom',
// setupFiles can extend expect with jest-dom matchers
// for assertions like toBeInTheDocument().
setupFiles: ['./test/setup.ts'],
},
});
// ============================================
// PART 2: TEST SETUP FILE
// ============================================
// test/setup.ts
import '@testing-library/jest-dom/vitest';
// This import adds DOM-specific matchers to Vitest's expect.
// Without it, toBeInTheDocument() and toBeVisible() are undefined.
// ============================================
// PART 3: A UNIT TEST FOR A SYNCHRONOUS COMPONENT
// ============================================
// components/Button.test.tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from './Button';
describe('Button', () => {
it('renders its children', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
});
it('calls onClick when clicked', async () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click me</Button>);
// userEvent simulates a real user interaction,
// including focus and pointer events.
await userEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('is disabled when the disabled prop is true', () => {
render(<Button disabled>Click me</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
});
// ============================================
// PART 4: A UNIT TEST FOR A CLIENT COMPONENT WITH HOOKS
// ============================================
// components/Counter.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter';
describe('Counter', () => {
it('increments when the button is clicked', async () => {
render(<Counter />);
const button = screen.getByRole('button', { name: /increment/i });
expect(screen.getByText('Count: 0')).toBeInTheDocument();
await userEvent.click(button);
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
});
// ============================================
// PART 5: WHY ASYNC SERVER COMPONENTS CANNOT BE UNIT TESTED
// ============================================
// components/UserList.tsx — an async Server Component
export default async function UserList() {
// This fetch runs during render on the server.
const users = await fetch('https://api.example.com/users').then(r => r.json());
return (
<ul>
{users.map((user: { id: number; name: string }) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// __tests__/UserList.test.tsx — this test will NOT work.
// import { render } from '@testing-library/react';
// import UserList from '../components/UserList';
//
// test('renders users', () => {
// render(<UserList />); // ❌ render() receives a Promise, not JSX
// });
//
// The fix is to test UserList through Playwright,
// or to extract the fetch into a separately tested function.
// ============================================
// PART 6: EXTRACTING DATA LOGIC FOR UNIT TESTING
// ============================================
// lib/users.ts
export async function fetchUsers() {
const response = await fetch('https://api.example.com/users');
if (!response.ok) {
throw new Error(`Failed to fetch users: ${response.status}`);
}
return response.json();
}
// __tests__/users.test.ts
import { describe, it, expect, vi } from 'vitest';
import { fetchUsers } from '../lib/users';
describe('fetchUsers', () => {
it('returns parsed JSON on success', async () => {
// Mock the global fetch for this test.
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve([{ id: 1, name: 'Alice' }]),
});
const users = await fetchUsers();
expect(users).toEqual([{ id: 1, name: 'Alice' }]);
});
it('throws on a non-OK response', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 500,
});
await expect(fetchUsers()).rejects.toThrow('Failed to fetch users: 500');
});
});
// ============================================
// PART 7: PLAYWRIGHT CONFIGURATION
// ============================================
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
// Run tests in parallel across files.
fullyParallel: true,
// Fail the build on CI if a test was accidentally left as test.only.
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
use: {
// baseURL lets tests use page.goto('/') instead of the full URL.
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
// webServer starts Next.js before tests run.
webServer: {
command: 'npm run build && npm run start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
// ============================================
// PART 8: A BASIC PLAYWRIGHT E2E TEST
// ============================================
// e2e/navigation.spec.ts
import { test, expect } from '@playwright/test';
test('navigates from home to about', async ({ page }) => {
await page.goto('/');
await page.click('text=About');
await expect(page).toHaveURL('/about');
await expect(page.locator('h1')).toContainText('About');
});
// ============================================
// PART 9: PLAYWRIGHT WITH THE instant() HELPER
// ============================================
// e2e/instant-navigation.spec.ts
import { test, expect } from '@playwright/test';
import { instant } from '@next/playwright';
test('settings page shows shell before data loads', async ({ page }) => {
await page.goto('/');
await instant(page, async () => {
await page.click('a[href="/settings"]');
// Assert on the shell: heading and skeleton are visible.
await expect(page.getByRole('heading', { name: 'Settings' })).toBeVisible();
await expect(page.getByTestId('settings-form-skeleton')).toBeVisible();
// The actual form is not visible because dynamic data is deferred.
await expect(page.getByTestId('settings-form')).toHaveCount(0);
});
// After instant() returns, the form streams in.
await expect(page.getByTestId('settings-form')).toBeVisible();
});
// ============================================
// PART 10: ENABLING THE TESTING API IN PRODUCTION BUILDS
// ============================================
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
// Required for instant() to work in production builds.
// Only enable this in CI or preview environments.
exposeTestingApiInProductionBuild: true,
},
};
module.exports = nextConfig;
// The cookie mechanism inside instant() sets:
// document.cookie = 'next-instant-navigation-testing=1; path=/'
// and clears it when the callback completes.
// This is the entire protocol — a single cookie .
The ten parts covered the full testing setup: Vitest configuration, a test setup file, unit tests for synchronous components, the async Server Component limitation, extracting data logic for independent testing, Playwright configuration, a basic E2E test, the instant() helper, and the production opt-in. The through-line is that each tool has a defined scope, and the tests that work are the ones that respect those boundaries.
Quick Reference
Testing layer responsibilities
| Layer | Tool | What it tests | What it cannot test |
|---|---|---|---|
| Unit | Vitest + RTL | Synchronous components, hooks, utilities | Async Server Components, streaming |
| E2E | Playwright | Navigation, Server Actions, full runtime | Isolated logic (too slow) |
| E2E + instant | Playwright + @next/playwright | Cached shell before dynamic data | Network timing (assumes warm cache) |
Vitest setup essentials
| Package | Purpose |
|---|---|
vitest | Test runner |
@vitejs/plugin-react | JSX transformation |
jsdom | Browser environment |
@testing-library/react | Component rendering |
@testing-library/jest-dom | DOM matchers |
vite-tsconfig-paths | @/ import resolution |
instant() helper behavior
| Phase | Cookie state | What renders |
|---|---|---|
| Before callback | Not set | Normal streaming behavior |
| Inside callback | next-instant-navigation-testing=1 | Cached shell only |
| After callback | Cleared | Normal streaming resumes |
Playwright projects
| Project | Browser engine | Use case |
|---|---|---|
chromium | Chrome/Edge | Primary CI target |
firefox | Firefox | Cross-browser verification |
webkit | Safari | Apple ecosystem verification |
Best Practices
✅ Do This:
// Use Vitest for synchronous components and pure functions
render(<Button>Click</Button>); // ✅
// Extract data fetching to testable functions
export async function fetchUsers() { /* ... */ } // ✅
// Use Playwright for async Server Components
test('page renders data', async ({ page }) => { /* ... */ }); // ✅
// Use instant() for shell-before-data assertions
await instant(page, async () => { /* shell assertions */ }); // ✅
// Set baseURL in playwright.config.ts
use: { baseURL: 'http://localhost:3000' } // ✅
// Enable exposeTestingApiInProductionBuild only in CI
experimental: { exposeTestingApiInProductionBuild: true } // ✅
// Use userEvent for realistic interactions
await userEvent.click(screen.getByRole('button')); // ✅
❌ Don’t Do This:
// Don't try to unit test async Server Components
render(<AsyncServerComponent />); // ❌
// Don't mock everything in E2E tests
vi.mock('@/lib/db'); // ❌ (in Playwright)
// Don't use instant() without Cache Components enabled
await instant(page, async () => { /* ... */ }); // ❌
// Don't enable the testing API in live production
experimental: { exposeTestingApiInProductionBuild: true } // ❌ (if deployed)
// Don't skip the warm cache assumption
// instant() tests structure, not network speed // ❌
// Don't use getByTestId when a role query works
screen.getByTestId('submit-button'); // ❌ (prefer getByRole)
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Vitest test hangs on async Server Component | render() receives a Promise, not JSX | Extract data logic; test component via Playwright |
instant() callback assertions fail | Cache not warm; prefetches incomplete | Navigate to the page first; ensure use cache directives are present |
instant() not available in production | Testing API disabled by default | Set exposeTestingApiInProductionBuild: true in test environments |
Playwright test can’t find @next/playwright | Package not installed | npm install -D @next/playwright |
| Unit test passes but E2E fails | Component works in isolation; integration breaks | Add E2E test for the full flow |
instant() returns immediately | No navigations inside callback | Wrap the page.click() that triggers navigation |
Real-World Examples
1. Testing a Button component
render(<Button onClick={vi.fn()}>Submit</Button>);
expect(screen.getByRole('button', { name: 'Submit' })).toBeEnabled();
2. Testing a custom hook with renderHook
const { result } = renderHook(() => useCounter());
act(() => result.current.increment());
expect(result.current.count).toBe(1);
3. Mocking fetch in a utility test
global.fetch = vi.fn().mockResolvedValue({ ok: true, json: () => data });
const result = await fetchUsers();
4. Basic Playwright navigation test
await page.goto('/');
await page.click('text=About');
await expect(page).toHaveURL('/about');
5. Testing a Server Action via E2E
await page.fill('input[name="email"]', 'test@example.com');
await page.click('button[type="submit"]');
await expect(page.locator('.success')).toBeVisible();
6. instant() with Suspense fallback
await instant(page, async () => {
await page.click('a[href="/dashboard"]');
await expect(page.locator('[data-testid="loading"]')).toBeVisible();
});
7. Testing a form submission with Playwright
await page.fill('input[name="title"]', 'New Post');
await page.click('button:has-text("Save")');
await expect(page.locator('.post-title')).toContainText('New Post');
8. Vitest test for a formatting utility
expect(formatCurrency(1234.5)).toBe('$1,234.50');
expect(formatCurrency(0)).toBe('$0.00');
9. Testing accessibility with Playwright
await expect(page.locator('button')).toHaveAccessibleName('Submit');
10. Configuring Vitest path aliases
plugins: [tsconfigPaths()] // resolves @/lib/utils to ./lib/utils
Visual
The Testing Pyramid in Next.js
┌──────────────────────────────────────────────────────────────┐
│ TESTING LAYERS │
│ │
│ ┌─────────────────────┐ │
│ │ E2E Tests │ │
│ │ (Playwright) │ │
│ │ │ │
│ │ Full browser, │ │
│ │ streaming, Server │ │
│ │ Actions, cookies │ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ instant() Tests │ │
│ │ │ │
│ │ Shell before data │ │
│ │ (Cache Components) │ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ Unit Tests │ │
│ │ (Vitest) │ │
│ │ │ │
│ │ Sync components, │ │
│ │ hooks, utilities │ │
│ └─────────────────────┘ │
│ │
│ More tests at the bottom. More confidence at the top. │
└──────────────────────────────────────────────────────────────┘
The Async Server Component Boundary
┌──────────────────────────────────────────────────────────────┐
│ WHAT VITEST CAN AND CANNOT RENDER │
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ Synchronous │ │ Async │ │
│ │ Component │ │ Server Component │ │
│ │ │ │ │ │
│ │ return <div /> │ │ async function │ │
│ │ │ │ return <div /> │ │
│ └──────────┬──────────┘ └──────────┬──────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ render() works │ │ render() receives │ │
│ │ assertions run │ │ a Promise │ │
│ │ ✅ Vitest │ │ ❌ Vitest │ │
│ └─────────────────────┘ └──────────┬──────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Test via Playwright│ │
│ │ or extract logic │ │
│ └─────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
How instant() Works
┌──────────────────────────────────────────────────────────────┐
│ THE instant() COOKIE PROTOCOL │
│ │
│ await instant(page, async () => { │
│ // 1. Set cookie │
│ // next-instant-navigation-testing=1 │
│ // │
│ // 2. Router serves only cached/prefetched content │
│ // Dynamic data is deferred │
│ // │
│ // 3. Assertions run against the shell │
│ // │
│ // 4. Cookie cleared on callback exit │
│ }); │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Cookie set │───▶│ Shell only │───▶│ Cookie │ │
│ │ │ │ renders │ │ cleared │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ A single cookie controls the entire behavior. │
│ The helper is intentionally thin so other tools │
│ can replicate it. │
└──────────────────────────────────────────────────────────────┘
The Test File Structure
┌──────────────────────────────────────────────────────────────┐
│ PROJECT TEST ORGANIZATION │
│ │
│ my-app/ │
│ ├── __tests__/ ← Vitest unit tests │
│ │ ├── page.test.tsx │
│ │ └── utils.test.ts │
│ ├── components/ │
│ │ └── Button.test.tsx ← Colocated unit tests │
│ ├── e2e/ ← Playwright E2E tests │
│ │ ├── navigation.spec.ts │
│ │ └── instant.spec.ts │
│ ├── vitest.config.mts ← Unit test configuration │
│ └── playwright.config.ts ← E2E configuration │
│ │
│ Unit tests live near the code. │
│ E2E tests live in a dedicated directory. │
│ The configs are separate because the runtimes are separate. │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Unit testing framework | Vitest + React Testing Library |
| E2E testing framework | Playwright |
| Async Server Component support in Vitest | Not supported; use E2E |
instant() package | @next/playwright |
instant() requirement | Cache Components enabled |
instant() mechanism | Single cookie: next-instant-navigation-testing |
instant() cache assumption | Warm cache (prefetches complete) |
| Production opt-in | experimental.exposeTestingApiInProductionBuild |
| Playwright baseURL | Set in playwright.config.ts |
| Test file locations | __tests__/ or colocated for Vitest; e2e/ for Playwright |
Key takeaways:
- Vitest cannot render async Server Components. The
render()function receives a Promise, not JSX. This is a boundary of the testing model, not a configuration issue. Extract data fetching into testable functions and test the component’s output through Playwright . instant()tests structure, not speed. The helper sets a cookie that serves only cached content during the callback. If content you expected to be cached is missing, the cause is a structural gap: a missinguse cachedirective, a misplaced Suspense boundary, or an incomplete prefetch .- The testing API is disabled in production by default.
instant()works in development without configuration, but production builds requireexperimental.exposeTestingApiInProductionBuild: true. This should only be enabled in CI or preview environments, never on a live site . - Playwright should run against a production build. The Next.js documentation recommends
npm run build && npm run startbefore running Playwright tests, because the production runtime behaves differently from the development server—prefetching is enabled, and the testing API is available only if opted in . - Each layer answers a different question. Unit tests answer “does this component render correctly given these props?” E2E tests answer “can a user accomplish this task in a real browser?”
instant()answers “does the shell render before dynamic data arrives?” A test suite that uses the right tool for each question is both faster and more reliable than one that forces everything through a single layer.
Remember: The temptation with testing is to find one tool and use it for everything. Vitest is fast and pleasant, so developers try to push async Server Components through it and hit the wall. Playwright is thorough, so developers try to test every utility function through a browser and watch the suite take ten minutes. The strategy that works is a division of labor. Vitest owns the synchronous, isolated, fast-feedback tests: components, hooks, pure functions. Playwright owns the integration tests: navigation, streaming, Server Actions, session cookies. The instant() helper sits at the boundary between “the page loaded” and “the data arrived,” and it exists because that boundary is where perceived performance lives. Testing it requires a tool that understands the difference between the shell and the stream.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!