💭

Playwright が `SyntaxError: Invalid or unexpected token` 連発する原因と完全解決

に公開

はじめに

AI にコードを書かせても、なぜかテストが一行も動かない時の対処法。
「Copilot が生成した Playwright テストが、SyntaxError: Invalid or unexpected token で全部落ちる」

これは、ロジックの問題ではなく、**不可視トークン(BOM / UTF-16 / CRLF)**が原因のことが多い。
特に Windows 環境×自動生成コード の組み合わせでは、この罠にハマりやすいので解消方法を記載する。

症状(実ログ)

=== Running spec:  ===
SyntaxError: Invalid or unexpected token
SyntaxError: Invalid or unexpected token
...

テストが1件も走らない/テストランナーの起動前に落ちる。


結論(最短解)

原因は、テストソースに混入した「BOM(UTF-8 BOM / UTF-16)」や CRLF 混在などのエンコーディング不整合。
UTF-8(BOMなし)+ LF 改行に正規化すると解決します(スクリプトは後述)。


まずは全体像(どこで壊れる?)

  • 先頭数バイトの BOM(0xEF,0xBB,0xBF 等)UTF-16 で保存されたファイルが混じると、パーサが**「見えない文字」を最初のトークン**として誤解釈 → Invalid/Unexpected token
  • CRLF 混在は二次障害(マッピングずれ・プラグイン誤作動)を誘発し、原因追跡を困難にします。

なぜ疑うのが妥当?(関連 Issue から学ぶ)

事例 ポイント 状態
Playwright: Invalid or unexpected token(空行の有無で挙動が変わる報告) 不可視文字の混入を示唆。再現が難しいが、見えないトークン起因の典型例。 (GitHub) Open/Closed 混在
Playwright: Unexpected token 'with' ランタイム/モード差で“Unexpected token”が出る別系統の報告(構文サポート問題)。BOMとは別因だが、症状が似る。 (GitHub) Closed
parse5: UTF-8 の BOM でパースが乱れる BOMがパーサを壊す実例。ツールにより扱いが分かれる。 (GitHub) Open
GitHub Actions: BOM でワークフローパーサが壊れる 設定ファイルでも BOMが致命傷になり得る。 (GitHub) Discussion
Vite: package.jsonUTF-8 BOM だと読み取り失敗 近年の報告。BOM が JSON 読み取りを破壊。 (GitHub) Open

以上から、**「不可視文字(BOM)/UTF-16/CRLF 異常 → Unexpected/Invalid token」**は複数プロダクトで観測される“普遍的な落とし穴”。


ファイル状態の見方(何が“危険”?)

判定 バイト列先頭 典型的な保存形式 影響
安全 UTF-8(BOMなし)× LF Playwright/バンドラの前提に合致
注意 0xEF 0xBB 0xBF UTF-8 BOM付き 先頭で不可視文字→Unexpected tokenの原因に
危険 0xFF 0xFE / 0xFE 0xFF UTF-16 LE/BE ほぼ確実に失敗(文字化け/解釈不能)
注意 改行 \r\n CRLF 混在 解析・マッピングがズレやすい(副作用)

解決の流れ(破壊なし・上書きのみ)

1) 検出レポート(BOM/空ファイルの棚卸し)

$patterns = '*.ts','*.tsx','*.js','*.jsx'
$files = Get-ChildItem -Path . -Recurse -Include $patterns -File

function Get-BomType {
  param([byte[]]$bytes)
  if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { return 'UTF8-BOM' }
  if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) { return 'UTF16-LE' }
  if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) { return 'UTF16-BE' }
  return 'None'
}

$report = foreach ($f in $files) {
  $bytes = [System.IO.File]::ReadAllBytes($f.FullName)
  [PSCustomObject]@{
    Path    = $f.FullName
    Size    = $bytes.Length
    Bom     = Get-BomType $bytes
    IsEmpty = ($bytes.Length -eq 0)
  }
}

$report | Sort-Object Bom, Path | Format-Table -AutoSize
$report | Export-Csv -NoTypeInformation -Encoding UTF8 "encoding_report.csv"
Write-Host "`nCSVに保存しました: encoding_report.csv"

結果

Path                                                       Size Bom  IsEmpty
----                                                                                                                              ---- ---  -------
C:\Users\Frontend\tests\simple-with-testids.spec.ts               0 None    True
C:\Users\Frontend\customers.ts                             3771 None   False

2) 正規化(UTF-8 no BOMLF に統一)

  • 自動判別で読み込み(UTF-16/UTF-8 BOM も拾う)→ LF 正規化 → **UTF-8(BOMなし)**で上書き。
  • 空ファイルはスキップ(削除しない/.bak も作らない)。
# UTF-8 (no BOM)
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)

# 既に読み込んだ$filesを再利用
foreach ($f in $files) {
  try {
    # BOMやUTF-16など自動検出ありで読み込み
    $sr = New-Object System.IO.StreamReader($f.FullName, $true)
    $text = $sr.ReadToEnd()
    $sr.Close()

    # 空ファイルはそのまま維持
    if ($text.Length -eq 0) {
      Write-Host "[Skip: Empty] $($f.FullName)"
      continue
    }

    # 改行をLFに正規化
    $normalized = $text -replace "`r`n","`n" -replace "`r","`n"

    # UTF-8 (BOMなし)で上書き
    [System.IO.File]::WriteAllText($f.FullName, $normalized, $utf8NoBom)

    Write-Host "[OK] $($f.FullName)"
  }
  catch {
    Write-Warning "変換失敗: $($f.FullName) - $($_.Exception.Message)"
  }
}

3) 事後チェック(BOM が消えたか?)

$report = foreach ($f in $files) {
  $bytes = [System.IO.File]::ReadAllBytes($f.FullName)
  [PSCustomObject]@{
    Path = $f.FullName
    Bom  = if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {'UTF8-BOM'} else {'None'}
  }
}
$report | Group-Object Bom | Format-Table Count, Name -AutoSize

Name = None のみになれば OK。Playwright を再実行するとエラーが消えます。


再発防止(チーム&CI で守る)

# .editorconfig
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.{ts,tsx,js,jsx}]
indent_style = space
indent_size = 2
# .gitattributes
* text=auto eol=lf
*.png binary
*.jpg binary
*.ico binary

Windows では git config core.autocrlf false 推奨。
さらに pre-commit フックCI(Azure Pipelines)で BOM 検知→コミット/ビルドを落とすと安心。


類似エラーの切り分け(構文サポート由来)

  • import x from './config.json' with { type: 'json' } など ESM/Node のサポート状況によっては “Unexpected token 'with'” 等が出ます。BOMと無関係の別因なので、BOM 正規化後に再現するかで切り分けましょう。 (GitHub)

参考 Issue / 議論(一次情報)

  • Playwright: Invalid or unexpected token(不可視文字が疑われる挙動の報告) (GitHub)
  • Playwright: Unexpected token 'with'(構文サポート差による別因) (GitHub)
  • parse5: UTF-8 BOM でパースが乱れる 実例 (GitHub)
  • GitHub Actions: BOM でパーサが壊れる 議論 (GitHub)
  • Vite: package.json が UTF-8 BOM だと読めない不具合 (GitHub)

まとめ(運用指針)

  • まずは正規化(UTF-8 no BOM + LF)
  • Editor/Git/Hook/CI多層ガードで再発を防止。
  • まだ落ちる場合は、**構文サポート差(ESM/CJS, Node/Playwright のバージョン)**を疑う。

Accenture Japan (有志)

Discussion