vitest icon indicating copy to clipboard operation
vitest copied to clipboard

fix: validation for arraykeys in suite for it.each cases

Open DevJoaoLopes opened this issue 5 months ago • 1 comments

Description

Change validation within suite.ts to fix switching issue in scenarios using it.each.

Used an early return to prevent tests from falling into an incorrect flow.

fixes #8189

Please don't delete this checklist! Before submitting the PR, please make sure you do the following:

  • [x] It's really useful if your PR references an issue where it is discussed ahead of time. If the feature is substantial or introduces breaking changes without a discussion, PR might be closed.
  • [x] Ideally, include a test that fails without this PR but passes with it.
  • [x] Please, don't make changes to pnpm-lock.yaml unless you introduce a new test example.

Tests

  • [x] Run the tests with pnpm test:ci.

Documentation

  • [x] If you introduce new functionality, document it. You can run documentation with pnpm run docs command.

Changesets

  • [x] Changes in changelog are generated from PR name. Please, make sure that it explains your changes in an understandable manner. Please, prefix changeset messages with feat:, fix:, perf:, docs:, or chore:.

DevJoaoLopes avatar Jun 19 '25 21:06 DevJoaoLopes

Deploy Preview for vitest-dev ready!

Built without sensitive environment variables

Name Link
Latest commit 2337581af0d4efd1f19638866957d08acfab0a0c
Latest deploy log https://app.netlify.com/projects/vitest-dev/deploys/686add37271d040008ce7cba
Deploy Preview https://deploy-preview-8192--vitest-dev.netlify.app
Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

netlify[bot] avatar Jun 19 '25 21:06 netlify[bot]

@hi-ogawa As I had tagged you in the issue debate, can you give me some support on how we will proceed with the adjustment?

DevJoaoLopes avatar Jul 06 '25 20:07 DevJoaoLopes

Can you also add test case from #8189 (comment)?

Done @hi-ogawa

DevJoaoLopes avatar Aug 16 '25 19:08 DevJoaoLopes

Thanks for the effort, however this only changes the order of expansion from % -> $ to $ -> % and that's not a proper solution as I wrote in https://github.com/vitest-dev/vitest/issues/8189#issuecomment-3088609224. For example, this previously passed but now fails:

test.for([{ a: '%o' }])('$a', (_, ctx) => {
  expect(ctx.task.name).toBe("'%o'")
// Expected: "'%o'"
// Received: "'{ a: '%o' }'"
});

Also please add an test case in a way it satisfies the 2nd checkbox in PR description. You only added only examples and they don't fail on current main.

[ ] Ideally, include a test that fails without this PR but passes with it.

There are similar test cases already in https://github.com/vitest-dev/vitest/blob/c1ac15c6b22903cf17b5b105dffd0b8aabf083c3/test/reporters/tests/default.test.ts#L202

hi-ogawa avatar Aug 18 '25 06:08 hi-ogawa

Thanks for the effort, however this only changes the order of expansion from % -> $ to $ -> % and that's not a proper solution as I wrote in #8189 (comment). For example, this previously passed but now fails:

test.for([{ a: '%o' }])('$a', (_, ctx) => {
  expect(ctx.task.name).toBe("'%o'")
// Expected: "'%o'"
// Received: "'{ a: '%o' }'"
});

Also please add an test case in a way it satisfies the 2nd checkbox in PR description. You only added only examples and they don't fail on current main.

[ ] Ideally, include a test that fails without this PR but passes with it.

There are similar test cases already in

https://github.com/vitest-dev/vitest/blob/c1ac15c6b22903cf17b5b105dffd0b8aabf083c3/test/reporters/tests/default.test.ts#L202

@hi-ogawa It really makes sense, maybe it's more complex than we imagined 😄 . I implemented an idea, can I send the code here for you to take a look at or do you prefer that I commit it?

DevJoaoLopes avatar Aug 18 '25 07:08 DevJoaoLopes

Yes, this is tricky and I'm not sure the fix is that simple either. What I have in mind would roughly look like a following process:

"$a %o $b %s"

  ⇓ (replace $ with placeholder)

"__tmp1__ %o __tmp2__ %s"

  ⇓ (apply % formatting)

"__tmp1__ xxx __tmp2__ yyy"

---

"$a"  "$b"

  ⇓    ⇓ (apply $ formatting individually)

"www" "zzz"

---

"__tmp1__ xxx __tmp2__ yyy"

  ⇓ (restore $ placeholder)

"www xxx zzz yyy"

hi-ogawa avatar Aug 18 '25 08:08 hi-ogawa

my idea would be the following, looking at the code:

function formatTitle(template: string, items: any[], idx: number) {
  // Step 1: Replace escaped percent signs
  template = template.replace(/%%/g, '__vitest_escaped_%__')

  // Step 2: Replace % expansions
  if (template.includes('%#') || template.includes('%$')) {
    template = template
      .replace(/%#/g, `${idx}`)
      .replace(/%\$/g, `${idx + 1}`)
  }

  // Step 3: Replace $ expansions
  // We want to expand $vars in the template, but only outside of %f or other %... expansions
  // We'll split the template into segments: those inside %f (or other %X) and those outside
  // For simplicity, let's process $expansions everywhere, but only if not inside a %... sequence

  // Find all %f, %s, etc. and replace them with placeholders
  const percentRegex = /%[a-z]/gi
  const percentMatches: RegExpMatchArray | null = template.match(percentRegex)
  const percentCount = percentMatches ? percentMatches.length : 0

  // Replace $expansions
  const isObjectItem = isObject(items[0])
  template = template.replace(
    /\$([$\w.]+)/g,
    (_, key: string) => {
      const isArrayKey = /^\d+$/.test(key)
      if (!isObjectItem && !isArrayKey) {
        return `$${key}`
      }
      const arrayElement = isArrayKey ? objectAttr(items, key) : undefined
      const value = isObjectItem ? objectAttr(items[0], key, arrayElement) : arrayElement
      return objDisplay(value, {
        truncate: runner?.config?.chaiConfig?.truncateThreshold,
      }) as unknown as string
    },
  )

  // Step 4: Restore escaped percent signs
  template = template.replace(/__vitest_escaped_%__/g, '%')

  // Step 5: Handle %f special case for -0 and NaN
  if (template.includes('%f')) {
    const placeholders = template.match(/%f/g) || []
    let temporaryTemplate = template
    placeholders.forEach((_, i) => {
      if (isNegativeNaN(items[i]) || Object.is(items[i], -0)) {
        let occurrence = 0
        temporaryTemplate = temporaryTemplate.replace(/%f/g, (match) => {
          occurrence++
          return occurrence === i + 1 ? '-%f' : match
        })
      }
    })
    return format(temporaryTemplate, ...items.slice(0, percentCount))
  }

  return format(template, ...items.slice(0, percentCount))
}

@hi-ogawa

DevJoaoLopes avatar Aug 18 '25 08:08 DevJoaoLopes

Please feel free to update the code and let's see the test case 🙂

hi-ogawa avatar Aug 18 '25 08:08 hi-ogawa

Please feel free to update the code and let's see the test case 🙂

I uploaded the code I commented on.

Regarding the reporter tests, I inserted it into the file you mentioned, but I'm not sure if it's the best place. I can change it if I see fit

@hi-ogawa

DevJoaoLopes avatar Aug 24 '25 22:08 DevJoaoLopes