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ès

La 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 created

Le ...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 base

Pattern 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 fonction

Rest : on collecte des choses depuis (on rassemble dans un paramètre).

function 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'objet

Mê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' — attention

Pour 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.

→ See also: Objets JavaScript et Déstructuration pour les Ingénieurs QA | JavaScript pour les QA Engineers: Le Minimum pour Commencer à Automatiser | TypeScript pour QA: Pourquoi les Types Statiques Améliorent Vos Tests