Senior Frontend Interview Guide, Part 12: Testing with Jest, React Testing Library, and Playwright
Enterprise teams ship frequently, and they can only do so safely with an automated test suite they trust. Interviewers ask about testing to learn whether you write tests that catch real bugs without slowing the team down. This chapter covers testing strategy, Jest, React Testing Library, mocking…

This article is Part 12 of 18 in The Senior Frontend Engineer Interview Guide, and includes 22 interview questions with model answers.
Enterprise teams ship frequently, and they can only do so safely with an automated test suite they trust. Interviewers ask about testing to learn whether you write tests that catch real bugs without slowing the team down. This chapter covers testing strategy, Jest, React Testing Library, mocking network calls with Mock Service Worker, testing hooks and stores, Playwright for end-to-end testing, visual and accessibility testing, and how to keep a test suite fast and reliable.
Testing Strategy
The Pyramid and the Trophy
The classic test pyramid recommends many unit tests, fewer integration tests, and few end-to-end tests. Kent C. Dodds’ testing trophy adapts it for frontend work: static analysis at the base, a modest number of unit tests, the largest share of integration tests (components rendered with their children and realistic data), and a smaller number of end-to-end tests at the top.
| Layer | Tools | Catches | Speed |
|---|---|---|---|
| Static | TypeScript, ESLint, Prettier | Typos, type errors, unsafe patterns | Instant |
| Unit | Jest/Vitest | Logic bugs in pure functions, reducers, utilities | Milliseconds |
| Integration (component) | Jest + React Testing Library + MSW | Behavior of components and features with mocked network | Fast |
| End-to-end | Playwright | Real browser, real routing, real (or staged) backend, cross-page flows | Seconds per test |
| Visual | Playwright screenshots, Chromatic, Percy | Unintended visual changes | Moderate |
| Accessibility | jest-axe, @axe-core/playwright | Detectable WCAG violations | Fast |
The guiding principle: “The more your tests resemble the way your software is used, the more confidence they can give you.” Test behavior, not implementation details.
What to Test
- Business rules: amount limits, fee calculations, validation, permission-based UI.
- User flows: can a user complete a transfer, see an error, recover?
- Edge cases: empty states, loading, errors, slow responses, large data, unusual locales.
- Regressions: every bug fixed gets a test.
What not to test: third-party library internals, implementation details like internal state names or which hook was called, and trivial code with no logic.
Jest Fundamentals
Jest is a test runner, assertion library, and mocking framework. Vitest is a fast, Vite-native alternative with a largely Jest-compatible API; most of this section applies to both.
// fees.test.js
import { calculateWireFee } from './fees';
describe('calculateWireFee', () => {
it('charges the domestic fee for domestic wires', () => {
expect(calculateWireFee({ type: 'domestic', amountCents: 500_00 })).toBe(25_00);
});
it.each([
['international', 100_00, 45_00],
['international', 1_000_000_00, 45_00],
])('charges a flat %s fee', (type, amountCents, expected) => {
expect(calculateWireFee({ type, amountCents })).toBe(expected);
});
it('rejects non-positive amounts', () => {
expect(() => calculateWireFee({ type: 'domestic', amountCents: 0 })).toThrow(RangeError);
});
});
Matchers You Should Know
| Matcher | Use |
|---|---|
toBe |
Object.is equality for primitives and references |
toEqual |
Recursive structural equality (ignores undefined properties) |
toStrictEqual |
Structural equality including undefined properties and class types |
toMatchObject |
Subset match |
toThrow |
Function throws (optionally matching type or message) |
toHaveBeenCalledWith, toHaveBeenCalledTimes |
Mock call assertions |
resolves / rejects |
Promise assertions: await expect(p).rejects.toThrow() |
expect.any(Number), expect.objectContaining() |
Asymmetric matchers |
toMatchSnapshot, toMatchInlineSnapshot |
Snapshot comparisons |
Setup and Teardown
beforeAll, beforeEach, afterEach, and afterAll run around tests within their describe block. Keep tests independent: each test should set up its own state and not rely on order.
Mocking
// Mock functions
const onSubmit = jest.fn();
onSubmit.mockResolvedValueOnce({ id: 'tx-1' });
// Spies on existing methods
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
// ...
spy.mockRestore();
// Module mocks
jest.mock('./analytics', () => ({ track: jest.fn() }));
import { track } from './analytics';
// Partial module mocks
jest.mock('./featureFlags', () => ({
...jest.requireActual('./featureFlags'),
isEnabled: jest.fn(() => true),
}));
jest.mock calls are hoisted above imports. Configure clearMocks or restoreMocks in the Jest config so mock state does not leak between tests.
Mock at the boundaries. Mock the network, time, randomness, and third-party SDKs; avoid mocking your own modules unless they are slow or nondeterministic. Over-mocking produces tests that pass while the real integration is broken.
Fake Timers
jest.useFakeTimers();
test('session warning appears after 13 minutes of inactivity', () => {
render(<SessionManager timeoutMs={15 * 60_000} warningMs={2 * 60_000} />);
act(() => { jest.advanceTimersByTime(13 * 60_000); });
expect(screen.getByRole('dialog', { name: /session expiring/i })).toBeInTheDocument();
});
With React Testing Library’s userEvent and fake timers, pass advanceTimers: jest.advanceTimersByTime to userEvent.setup().
Snapshots
Snapshots serialize output and compare it with a stored version. Large component snapshots are brittle and rarely reviewed carefully, so they tend to be updated blindly. Use small, targeted inline snapshots for serialized data (an error object, generated CSV) rather than whole component trees.
Configuration Essentials
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/test/setup.ts'],
moduleNameMapper: {
'\\.(css|scss)$': 'identity-obj-proxy', // CSS Modules return class names
'^@/(.*)$': '<rootDir>/src/$1',
},
transform: { '^.+\\.[jt]sx?$': ['@swc/jest'] }, // or babel-jest / ts-jest
collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/*.stories.tsx'],
coverageThreshold: { global: { branches: 75, lines: 80 } },
clearMocks: true,
};
identity-obj-proxy makes styles.primary return "primary", so CSS Modules work in tests.
React Testing Library
React Testing Library (RTL) renders components into a jsdom document and encourages querying the DOM the way users perceive it: by role, label, and text.
Query Priority
- Accessible to everyone:
getByRole(withname),getByLabelText,getByPlaceholderText,getByText,getByDisplayValue. - Semantic queries:
getByAltText,getByTitle. - Test IDs:
getByTestIdas a last resort.
getByRole is preferred because it also verifies accessibility: if you cannot find a button by role and name, neither can a screen reader user.
Query Variants
| Variant | No match | Multiple matches | Async |
|---|---|---|---|
getBy |
Throws | Throws | No |
queryBy |
Returns null |
Throws | No |
findBy |
Rejects after timeout | Rejects | Yes (polls until found) |
getAllBy / queryAllBy / findAllBy |
As above, returning arrays |
Use queryBy only to assert that something is not present. Use findBy for elements that appear asynchronously.
A Realistic Component Test
import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('user can review and submit a transfer', async () => {
const user = userEvent.setup();
renderWithProviders(<TransferPage />);
await user.selectOptions(await screen.findByLabelText(/from account/i), 'Checking ****4821');
await user.selectOptions(screen.getByLabelText(/to/i), 'Savings ****1199');
await user.type(screen.getByLabelText(/amount/i), '250.00');
await user.click(screen.getByRole('button', { name: /review transfer/i }));
const review = screen.getByRole('region', { name: /review/i });
expect(within(review).getByText('$250.00')).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: /confirm/i }));
expect(await screen.findByRole('status')).toHaveTextContent(/transfer scheduled/i);
});
Notes on good practice visible here:
userEvent(v14) simulates full interactions (pointer events, focus, keyboard) and is async; prefer it overfireEvent.- Use
screenrather than destructuring render results. - Assertions use
@testing-library/jest-dommatchers:toBeInTheDocument,toHaveTextContent,toBeDisabled,toHaveAccessibleName,toHaveAttribute. - No reliance on class names, component internals, or state.
A Custom Render with Providers
export function renderWithProviders(ui, { route = '/', queryClient, locale = 'en-US' } = {}) {
const client = queryClient ?? new QueryClient({
defaultOptions: { queries: { retry: false, gcTime: Infinity } },
});
window.history.pushState({}, '', route);
return render(
<QueryClientProvider client={client}>
<I18nProvider locale={locale}>
<MemoryRouter initialEntries={[route]}>{ui}</MemoryRouter>
</I18nProvider>
</QueryClientProvider>
);
}
Disabling retries avoids slow tests waiting for retry backoff when testing error states. Create a new QueryClient per test to avoid cache leakage.
Async Utilities and act
findBy*andwaitFor(() => expect(...))wait for asynchronous updates.- Do not put side effects inside
waitForcallbacks; only assertions. waitForElementToBeRemovedwaits for loading indicators to disappear.- RTL wraps renders and
userEventinteractions inact()automatically. “Not wrapped in act” warnings usually mean an asynchronous state update happened after the test finished asserting; the fix is to wait for the resulting UI change (await screen.findBy...), not to wrap things manually inact.
Testing Hooks
import { renderHook, act } from '@testing-library/react';
test('useDebouncedValue updates after delay', () => {
jest.useFakeTimers();
const { result, rerender } = renderHook(({ v }) => useDebouncedValue(v, 300), {
initialProps: { v: 'a' },
});
rerender({ v: 'ab' });
expect(result.current).toBe('a');
act(() => { jest.advanceTimersByTime(300); });
expect(result.current).toBe('ab');
});
Test hooks directly only when they are reusable building blocks; otherwise test them through the components that use them.
Mock Service Worker (MSW)
MSW intercepts network requests at the network layer (a Service Worker in the browser, request interception in Node) and returns mocked responses. Components and Axios code run unchanged, which makes tests realistic and mocks reusable across unit tests, Storybook, and local development.
// test/handlers.js
import { http, HttpResponse, delay } from 'msw';
export const handlers = [
http.get('/api/accounts', () =>
HttpResponse.json([
{ id: 'a1', nickname: 'Checking', mask: '4821', balance: { value: '1520.33', currency: 'USD' } },
])
),
http.post('/api/transfers', async ({ request }) => {
const body = await request.json();
if (Number(body.amount) > 10000) {
return HttpResponse.json(
{ title: 'Limit exceeded', errors: [{ field: 'amount', code: 'DAILY_LIMIT' }] },
{ status: 422 }
);
}
await delay(100);
return HttpResponse.json({ id: 'tr-1', status: 'scheduled' }, { status: 201 });
}),
];
// test/setup.ts
import { setupServer } from 'msw/node';
export const server = setupServer(...handlers);
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
Override per test to simulate errors:
test('shows an error when accounts fail to load', async () => {
server.use(http.get('/api/accounts', () => HttpResponse.json({}, { status: 503 })));
renderWithProviders(<AccountsPage />);
expect(await screen.findByRole('alert')).toHaveTextContent(/temporarily unavailable/i);
});
onUnhandledRequest: 'error' ensures that a component calling an unmocked endpoint fails loudly instead of silently hanging.
Playwright for End-to-End Testing
Playwright drives real Chromium, Firefox, and WebKit browsers with a single API. Its test runner (@playwright/test) includes parallelism, fixtures, retries, tracing, screenshots, video, and HTML reports.
Core Concepts
- Locators are lazy, auto-waiting references to elements:
page.getByRole('button', { name: 'Confirm' }). They re-resolve on each action, which avoids stale-element errors. - Auto-waiting: actions wait for elements to be attached, visible, stable, enabled, and receiving events before acting.
- Web-first assertions retry until they pass or time out:
await expect(locator).toBeVisible(),toHaveText,toHaveURL,toHaveCount. - Browser contexts are isolated, incognito-like sessions; each test gets a fresh context, making tests independent and parallelizable.
- Fixtures provide setup and teardown (
page,context,request, and custom ones likeauthenticatedPage).
An End-to-End Test
import { test, expect } from '@playwright/test';
test.describe('transfers', () => {
test('customer schedules an internal transfer', async ({ page }) => {
await page.goto('/transfers/new');
await page.getByLabel('From account').selectOption({ label: 'Checking ****4821' });
await page.getByLabel('To account').selectOption({ label: 'Savings ****1199' });
await page.getByLabel('Amount').fill('250.00');
await page.getByRole('button', { name: 'Review transfer' }).click();
await expect(page.getByRole('heading', { name: 'Review your transfer' })).toBeVisible();
await page.getByRole('button', { name: 'Confirm' }).click();
await expect(page.getByRole('status')).toContainText('Transfer scheduled');
await expect(page).toHaveURL(/\/transfers\/[a-z0-9-]+\/confirmation/);
});
});
Authentication Setup
Logging in through the UI in every test is slow. Playwright’s recommended approach is a setup project that logs in once and saves storage state:
// auth.setup.ts
import { test as setup, expect } from '@playwright/test';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Username').fill(process.env.E2E_USER!);
await page.getByLabel('Password').fill(process.env.E2E_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
await page.getByLabel('Verification code').fill(await getTestOtp());
await page.getByRole('button', { name: 'Verify' }).click();
await expect(page.getByRole('heading', { name: 'Accounts' })).toBeVisible();
await page.context().storageState({ path: 'playwright/.auth/user.json' });
});
// playwright.config.ts
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
reporter: [['html'], ['junit', { outputFile: 'results.xml' }]],
use: { baseURL: process.env.BASE_URL, trace: 'on-first-retry', screenshot: 'only-on-failure' },
projects: [
{ name: 'setup', testMatch: /auth\.setup\.ts/ },
{ name: 'chromium', use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/user.json' }, dependencies: ['setup'] },
{ name: 'webkit', use: { ...devices['Desktop Safari'], storageState: 'playwright/.auth/user.json' }, dependencies: ['setup'] },
{ name: 'mobile', use: { ...devices['Pixel 7'], storageState: 'playwright/.auth/user.json' }, dependencies: ['setup'] },
],
});
For MFA in test environments, teams use test users with TOTP secrets (generate codes in the test with a TOTP library), a test-only OTP retrieval API, or an IdP configuration that bypasses MFA for designated test accounts in non-production environments only.
Network Mocking and API Testing
test('shows maintenance banner when payments API is down', async ({ page }) => {
await page.route('**/api/payments/**', route =>
route.fulfill({ status: 503, json: { title: 'Service unavailable' } }));
await page.goto('/payments');
await expect(page.getByRole('alert')).toContainText('temporarily unavailable');
});
test('API creates payee', async ({ request }) => {
const res = await request.post('/api/payees', { data: { name: 'Electric Co', accountNumber: '123456789' } });
expect(res.status()).toBe(201);
});
Use the request fixture to create test data through APIs before UI tests (faster and more reliable than creating it through the UI) and to clean up afterward.
Page Object Model
For larger suites, encapsulate page interactions in classes or fixtures:
export class TransferPage {
constructor(private page: Page) {}
async goto() { await this.page.goto('/transfers/new'); }
async fill({ from, to, amount }: { from: string; to: string; amount: string }) {
await this.page.getByLabel('From account').selectOption({ label: from });
await this.page.getByLabel('To account').selectOption({ label: to });
await this.page.getByLabel('Amount').fill(amount);
}
async review() { await this.page.getByRole('button', { name: 'Review transfer' }).click(); }
}
Page objects keep tests readable and localize selector changes. Avoid putting assertions deep inside page objects; keep them in tests.
Debugging and Tracing
npx playwright test --uiopens UI mode with time-travel debugging.--debugruns headed with the Playwright Inspector.- Trace Viewer (
trace: 'on-first-retry') records actions, DOM snapshots, network, and console logs for failed tests in CI; open withnpx playwright show-trace. npx playwright codegenrecords interactions to generate a first draft of a test.
Visual Regression Testing
test('account summary card matches baseline', async ({ page }) => {
await page.goto('/storybook/iframe.html?id=accounts-summarycard--default');
await expect(page.getByTestId('summary-card')).toHaveScreenshot('summary-card.png', {
maxDiffPixelRatio: 0.01,
mask: [page.getByTestId('timestamp')],
});
});
Run visual tests in a consistent environment (the official Playwright Docker image) because fonts and rendering differ across operating systems. Mask dynamic content like dates.
Accessibility in Playwright
Use @axe-core/playwright (Chapter 10) on key pages and states, and write keyboard-only tests for critical flows:
await page.keyboard.press('Tab');
await expect(page.getByRole('link', { name: 'Skip to main content' })).toBeFocused();
Flaky Tests
A flaky test passes and fails without code changes. Flakiness destroys trust in the suite. Causes and remedies:
| Cause | Remedy |
|---|---|
Fixed sleeps (waitForTimeout(2000)) |
Web-first assertions and auto-waiting |
| Shared test data between parallel tests | Unique data per test; API-based setup and teardown |
| Dependence on test order | Independent tests with their own setup |
| Real third-party services | Mock or stub them; contract-test separately |
| Animations and transitions | Disable animations in test mode, reducedMotion: 'reduce' |
| Time and time zones | Fake timers, fixed timezoneId and locale in Playwright config, page.clock API |
| Brittle selectors (CSS classes, nth-child) | Role and label locators, test IDs as fallback |
| Environment instability | Health checks before runs, retries with traces, quarantine with tickets |
Retries hide flakiness; use them to keep CI green while tracking and fixing flaky tests, never as the fix itself.
Coverage and Quality Metrics
Code coverage tells you what code ran, not whether it was meaningfully tested. Set reasonable thresholds (often 70 to 85 percent) to catch untested areas, focus review on business-critical paths, and consider mutation testing (Stryker) for critical logic like fee calculations, which verifies that tests fail when code is changed.
Testing in CI
- Run lint, type checks, and unit/component tests on every pull request; they should finish in a few minutes.
- Shard large Jest and Playwright suites across CI machines (
--shard=1/4). - Run a smoke subset of end-to-end tests on pull requests and the full suite on merges or nightly.
- Publish JUnit reports, coverage, and Playwright HTML reports and traces as artifacts.
- Run end-to-end tests against preview environments deployed per pull request when possible.
Interview Questions and Answers
Q1. What is the difference between unit, integration, and end-to-end tests?
Unit tests check small pieces of logic in isolation. Integration tests check several units working together, such as a component with its children, hooks, and a mocked network layer. End-to-end tests run the full application in a real browser against a real or staged backend, verifying complete user flows. They trade speed and isolation for realism.
Q2. What is the testing trophy?
A model that emphasizes static analysis at the base, some unit tests, the most effort on integration tests, and fewer end-to-end tests, because integration tests give high confidence at reasonable cost for frontend applications.
Q3. Why does React Testing Library discourage testing implementation details?
Tests coupled to internals (state variable names, component instance methods, class names) break when you refactor without changing behavior, and they can pass while user-visible behavior is broken. Querying by role and label and interacting like a user produces tests that fail only when behavior changes.
Q4. Explain getBy, queryBy, and findBy.
getBy returns the element or throws if missing or ambiguous. queryBy returns null when missing, for asserting absence. findBy returns a promise that resolves when the element appears, for asynchronous UI. Each has an All variant for multiple elements.
Q5. Why prefer userEvent over fireEvent?
userEvent simulates complete interactions as a browser would, including focus changes, pointer and keyboard event sequences, and input behavior, so tests catch bugs that a single synthetic event would miss. fireEvent dispatches one event and is suitable only for events userEvent does not support.
Q6. How do you mock API calls in component tests?
With Mock Service Worker, which intercepts requests at the network level so the real Axios client and data-fetching hooks run. Define default handlers in a shared setup and override per test for error and edge cases. Set onUnhandledRequest: 'error' to catch missing mocks.
Q7. How do you fix “not wrapped in act(…)” warnings?
They indicate a state update occurred outside of RTL’s awareness, typically after the test’s assertions finished. Wait for the expected UI change with findBy or waitFor, ensure async work completes or is cleaned up, and mock timers appropriately. Manually wrapping in act is rarely the right fix.
Q8. How do you test a component that uses a Zustand store or Context?
Render it within the real providers using a custom render function, set up initial state via the store’s setState or provider props, reset store state between tests, and assert on visible behavior. Avoid mocking the store unless it hides an expensive dependency.
Q9. What makes Playwright tests less flaky than older tools?
Auto-waiting for actionability before each action, web-first assertions that retry, locators that re-resolve, isolated browser contexts per test, and built-in tracing for diagnosing failures. Tests still become flaky with shared data, fixed sleeps, or unstable environments.
Q10. How do you handle authentication in Playwright tests?
Use a setup project that logs in once (including MFA with test TOTP secrets or a test-only mechanism) and saves the storage state to a file, then configure test projects to reuse that storage state. For different roles, create multiple storage state files.
Q11. What is the Page Object Model?
A pattern that wraps page-specific locators and actions in classes or fixtures, so tests express intent (“fill the transfer form”) and selector changes are fixed in one place. Assertions generally stay in tests.
Q12. How do you test accessibility automatically?
Run axe-core via jest-axe in component tests and @axe-core/playwright in end-to-end tests, use role-based queries that implicitly check accessible names, add eslint-plugin-jsx-a11y, and write keyboard navigation tests for critical flows. Automated checks complement manual testing.
Q13. When are snapshot tests useful?
For small, stable serialized outputs where any change should be reviewed, such as generated data structures or error messages, using inline snapshots. Large component snapshots are brittle, noisy, and often updated without review, so I avoid them.
Q14. How do you test time-dependent behavior?
Use Jest fake timers (or Vitest’s vi.useFakeTimers) to control setTimeout, intervals, and Date, and Playwright’s page.clock to control time in the browser. Configure time zones explicitly in the test environment and in Playwright (timezoneId) to catch time zone bugs.
Q15. How do you deal with flaky tests in CI?
Treat them as defects: capture traces and logs on retry, identify the cause (timing, data, order, environment), fix it, and if needed quarantine the test temporarily with a tracking ticket. Avoid fixed waits and shared data. Track flakiness rates over time.
Q16. What is code coverage, and is 100 percent a good goal?
Coverage measures which lines, branches, or functions executed during tests. It is a useful indicator of untested code but not of test quality, and chasing 100 percent leads to low-value tests. Set reasonable thresholds and focus on critical paths, with mutation testing for important logic.
Q17. How would you test a micro frontend?
Component and integration tests within the remote using a mock shell and MSW, contract tests for exposed modules and events, standalone end-to-end tests of the remote, and integration end-to-end tests in a composed environment that includes the host and other remotes at the versions being released.
Q18. How do you visually test components?
Render each component state (often Storybook stories) in a consistent browser environment, capture screenshots, and compare with baselines using Playwright toHaveScreenshot, Chromatic, or Percy. Mask dynamic content, run in Docker for consistency, and review diffs in pull requests.
Q19. How do you test error states like a 500 from the server?
Override the MSW handler for that test to return a 500, render the component, and assert that the error UI appears with a recovery action. In Playwright, use page.route to fulfill with an error status. Disable retries in the query client for faster tests.
Q20. What is the difference between Jest and Vitest?
Both are test runners with similar APIs. Vitest is built on Vite, reuses the app’s Vite configuration and transforms, supports ES modules natively, and is typically faster in Vite projects. Jest is mature with a large ecosystem and is common in webpack-based code bases. Migration is usually straightforward.
Q21. How do you test a custom hook?
Use renderHook from React Testing Library, providing a wrapper for any required providers, and interact with the result through act. For hooks that are only used by one component, test them through that component instead.
Q22. How would you structure testing for a payment submission flow?
Unit tests for amount parsing, validation, fee calculation, and the state machine; component tests with MSW for the form, review, and confirmation steps including validation, limit errors, step-up MFA, and network failures; an end-to-end test for the happy path and a critical error path in a staging environment; accessibility checks on each step; and contract tests for the payment API.
Key Takeaways
- Invest most in integration tests that exercise real components with a mocked network.
- Query by role and label, interact with
userEvent, and assert on visible behavior. - Use MSW to mock the network realistically across tests, Storybook, and development.
- Playwright provides reliable cross-browser end-to-end tests with auto-waiting, fixtures, and tracing.
- Treat flaky tests as bugs; keep CI fast with sharding and tiered suites.
← Part 11: Internationalization and Localization · All chapters · Part 13: Git and CI/CD Pipelines →







