💭
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.json が UTF-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 BOM + LF に統一)
- 自動判別で読み込み(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 のバージョン)**を疑う。
Discussion