L'opérateur de propagation (...) déploie des tableaux et des objets à la volée, tandis que l'opérateur rest collecte les arguments restants dans un tableau. Deux comportements distincts partagent la même syntaxe.
Opérateur de propagation : déployer des valeurs
L'opérateur de propagation (...) déploie un itérable (tableau ou objet) à la volée.
Propager des tableaux
const part1 = [1, 2, 3];
const part2 = [4, 5, 6];
const combined = [...part1, ...part2];
// [1, 2, 3, 4, 5, 6]
// Ajouter des éléments avant ou après
const withExtra = [0, ...part1, ...part2, 7];
// [0, 1, 2, 3, 4, 5, 6, 7]Copier un tableau
const original = ['alice', 'bob', 'charlie'];
const copy = [...original];
copy.push('dave'); // Modifie uniquement copy
console.log(original); // ['alice', 'bob', 'charlie'] — inchangéPropager des objets
const baseConfig = { timeout: 30000, headless: true };
const ciConfig = { ...baseConfig, timeout: 60000 };
// { timeout: 60000, headless: true }
// Note : timeout a été écrasé par la valeur définie aprèsLa clé définie en dernier l'emporte en cas de conflit.
La propagation dans les données de test
Le pattern le plus utile pour les ingénieurs QA : créer des variantes d'objet depuis une base.
const defaultUser = {
email: 'test@example.com',
password: 'ValidPass1',
role: 'member',
isActive: true,
emailVerified: true,
};
// Créer des variantes sans répéter tous les champs
const adminUser = { ...defaultUser, role: 'admin' };
const inactiveUser = { ...defaultUser, isActive: false };
const unverified = { ...defaultUser, emailVerified: false };
const customEmail = { ...defaultUser, email: 'custom@test.com' };C'est la base des fabriques de données de test. On définit l'état du chemin heureux une seule fois, et on crée tous les cas limites à partir de lui.
Fusionner des configurations de test
const basePlaywrightConfig = {
use: {
baseURL: 'https://lab.becomeqa.com',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
timeout: 30_000,
};
// Surcharges pour l'environnement CI
const ciOverrides = {
timeout: 60_000,
retries: 2,
};
const finalConfig = { ...basePlaywrightConfig, ...ciOverrides };Construire des listes de cas de test
const happyPathCase = {
email: 'valid@example.com',
password: 'ValidPass1',
expectedResult: 'success',
expectedStatus: 200,
};
const testCases = [
happyPathCase,
{ ...happyPathCase, email: 'another@test.com' },
{ ...happyPathCase, password: 'AnotherValidPass2' },
{ ...happyPathCase, email: 'not-valid', expectedResult: 'error', expectedStatus: 422 },
];Beaucoup plus propre que de répéter tous les champs pour chaque cas de test.
Paramètres rest : collecter des valeurs
Les paramètres rest collectent plusieurs éléments dans un seul paramètre. Même syntaxe ..., mais dans une définition de fonction, pas dans un appel :
// REST : collecte plusieurs arguments dans un tableau
function logTestResults(testName: string, ...results: string[]) {
console.log(`Test: ${testName}`);
results.forEach(r => console.log(` - ${r}`));
}
logTestResults('Login test', 'email validated', 'redirect worked', 'session created');
// Test: Login test
// - email validated
// - redirect worked
// - session createdLe ...results collecte tous les arguments après testName dans un tableau.
Rest dans la déstructuration
Rest peut aussi s'utiliser dans la déstructuration pour collecter "tout le reste" :
const [first, second, ...rest] = [1, 2, 3, 4, 5];
// first = 1, second = 2, rest = [3, 4, 5]
const { email, password, ...otherFields } = user;
// email et password sont extraits
// otherFields = { role: 'member', isActive: true, ... }Utile quand on a besoin de champs précis et qu'on veut passer le reste plus loin :
async function createUserAndGetToken({ email, password, ...profileData }: UserCreationData) {
// Utiliser email et password pour l'authentification
const token = await auth.login(email, password);
// Utiliser le reste pour configurer le profil (sans les champs d'auth)
await api.updateProfile(token, profileData);
return token;
}Patterns pratiques dans Playwright
Pattern 1 : helper de clic flexible
type ClickOptions = {
timeout?: number;
force?: boolean;
};
async function clickAndWait(
page: Page,
selector: string,
{ timeout = 5000, force = false, ...options }: ClickOptions = {}
) {
await page.locator(selector).click({ timeout, force, ...options });
await page.waitForLoadState('networkidle');
}Pattern 2 : construire des headers de requête
const defaultHeaders = {
'Content-Type': 'application/json',
'Accept': 'application/json',
};
const authHeaders = {
...defaultHeaders,
'Authorization': `Bearer ${token}`,
};
const adminHeaders = {
...authHeaders,
'X-Admin-Key': process.env.ADMIN_KEY,
};Pattern 3 : combiner des données de test depuis plusieurs sources
const baseData = await readFixtureFile('base-user.json');
const envSpecific = await readFixtureFile(`${process.env.ENV}-overrides.json`);
const testData = { ...baseData, ...envSpecific };
// Les valeurs spécifiques à l'environnement écrasent les valeurs de basePattern 4 : collecter les cas en échec
async function runAllCases(cases: TestCase[]): Promise<string[]> {
const failures: string[] = [];
for (const testCase of cases) {
try {
await runCase(testCase);
} catch (error) {
failures.push(`${testCase.name}: ${error.message}`);
}
}
return failures;
}
// Utilisation :
const [firstFailure, ...otherFailures] = await runAllCases(testCases);
if (firstFailure) {
console.log('First failure:', firstFailure);
console.log('Additional failures:', otherFailures);
}Spread vs. rest : comment les distinguer
Spread : on met des choses dedans (on déploie dans un tableau ou un objet littéral).const arr = [...items]; // déploie items dans le tableau
const obj = { ...config }; // déploie config dans l'objet
func(...args); // déploie args comme arguments de fonctionfunction fn(...args) {} // collecte les arguments d'appel
const [a, ...rest] = array; // collecte le reste du tableau
const { x, ...others } = obj; // collecte le reste de l'objetMême symbole .... Le contexte détermine la signification.
Une erreur courante : la copie superficielle
La propagation crée une copie superficielle : les objets imbriqués restent des références partagées.
const user = { name: 'Alice', address: { city: 'NYC' } };
const copy = { ...user };
copy.name = 'Bob'; // ✅ Modifie uniquement copy
copy.address.city = 'London'; // ⚠️ Modifie BOTH user et copy
console.log(user.address.city); // 'London' — attentionPour une copie profonde (les objets imbriqués sont aussi indépendants), il faut une autre approche :
// Copie profonde simple (fonctionne pour les données JSON-sérialisables)
const deepCopy = JSON.parse(JSON.stringify(user));
// Ou utiliser structuredClone (JS moderne)
const deepCopy2 = structuredClone(user);Pour la plupart des objets de données de test simples sans objets imbriqués mutables, la propagation convient. Mais connaître cette limite évite des surprises.
Récapitulatif
| Syntaxe | Contexte | Ce qu'elle fait |
|---------|---------|----------------|
| [...arr] | Tableau littéral | Déploie arr dans le nouveau tableau |
| {...obj} | Objet littéral | Copie toutes les propriétés de obj |
| fn(...args) | Appel de fonction | Passe les éléments du tableau comme arguments séparés |
| function fn(...params) | Définition de fonction | Collecte plusieurs arguments dans un tableau |
| const [a, ...rest] = arr | Déstructuration de tableau | Collecte les éléments restants |
| const {x, ...rest} = obj | Déstructuration d'objet | Collecte les propriétés restantes |
Une fois que le pattern est assimilé, on le reconnaît partout. Il est présent dans use() de Playwright, dans les fixtures de test, dans les fichiers de config, et dans chaque fabrique de données de test.