[{"data":1,"prerenderedAt":1375},["ShallowReactive",2],{"search-dialog-posts":3,"post-\u002Fposts\u002Fplaywright-vs-cypress":316,"post-surround-\u002Fposts\u002Fplaywright-vs-cypress":1368},[4,12,21,29,37,47,57,67,77,85,96,104,112,121,129,137,147,155,164,173,181,188,195,207,215,224,236,241,247,254,260,266,272,279,286,291,298,304,310],{"path":5,"title":6,"description":7,"category":8,"tags":9},"\u002Fposts\u002Fgit-commands-cheatsheet","Git 常用指令與終端機筆記","2018 年整理的 Git 學習筆記，包含基本終端機指令、Git 操作、分支、reset、SourceTree、Vim 與一些常用設定。","notes",[10,11],"Git","版本控制",{"path":13,"title":14,"description":15,"category":16,"tags":17},"\u002Fposts\u002Fjs-prototype-inheritance","JavaScript 物件導向：建構函式、prototype 與繼承三步驟","整理 ES5 時代用建構函式加 prototype 手刻物件導向的完整流程——封裝屬性與方法、共用函式為什麼要掛在 prototype 上、以及繼承的三個步驟（call 借用母類別建構子、Object.create 接上原型鏈、修正 constructor 指向），並附上物件縮寫與計算屬性這兩個順便學到的 ES6 寫法。","frontend",[18,19,20],"JavaScript","物件導向","prototype",{"path":22,"title":23,"description":24,"category":16,"tags":25},"\u002Fposts\u002Fflexbox-notes","Flexbox 對齊與彈性尺寸筆記","記錄 Flexbox 的主軸與交錯軸對齊、flex-direction、flex-wrap，以及 flex-grow、flex-shrink、flex-basis 等常用屬性。",[26,27,28],"CSS","Flexbox","版面",{"path":30,"title":31,"description":32,"category":16,"tags":33},"\u002Fposts\u002Fscreen-orientation-api","screen.orientation：現在判斷與鎖定螢幕方向的做法","整理現在 JavaScript 判斷螢幕方向的方式，包括 screen.orientation、change 事件與 lock()，並說明 RWD 使用 matchMedia 或 CSS orientation 會更適合。",[18,34,35,36],"RWD","螢幕方向","PWA",{"path":38,"title":39,"description":40,"category":16,"tags":41},"\u002Fposts\u002Fpinia-vuex-state-management","Pinia \u002F Vuex 狀態管理及差異","紀錄 Vuex、Pinia 的狀態管理概念、差異與用法。",[42,43,44,45,46],"Pinia","Vuex","Vue3","狀態管理","Nuxt",{"path":48,"title":49,"description":50,"category":8,"tags":51},"\u002Fposts\u002Fpyenv-python-version-management","Python 多版本管理 pyenv","紀錄 macOS 與 Windows 使用 pyenv 管理 Python 版本的方式。",[52,53,54,55,56],"Python","pyenv","macOS","Windows","版本管理",{"path":58,"title":59,"description":60,"category":8,"tags":61},"\u002Fposts\u002Fnpm-publish-vue2-component","npm 發布流程","紀錄 Vue 2 元件的 npm 發布、dist-tag 與打包流程。",[62,63,64,65,66],"npm","Vue2","Vue CLI","套件發布","MIT",{"path":68,"title":69,"description":70,"category":8,"tags":71},"\u002Fposts\u002Fmobile-debugging-today","現在怎麼 debug 手機網頁：從裝一堆工具到幾乎不用裝","對照 2022 年那套 scoop、iTunes、remotedebug-ios-webkit-adapter 的繁瑣設定，整理現在偵錯手機網頁的做法：Android 無線偵錯、iOS 的實際選項、on-device console、Playwright 裝置模擬，以及本機 HTTPS 的處理。",[72,73,74,75,76],"debug","iOS","Android","Chrome DevTools","Playwright",{"path":78,"title":79,"description":80,"category":16,"tags":81},"\u002Fposts\u002Fplaywright-vs-cypress","E2E 測試現在選什麼：Playwright 與 Cypress 的實際差異","對照幾年前用 Cypress 的做法，說明 Playwright 為什麼變成多數新專案的預設選擇：真正的多瀏覽器支援、免費的平行執行、trace viewer 除錯，以及兩者在架構上的根本差異與各自仍然適合的場景。",[76,82,83,84],"Cypress","E2E","測試",{"path":86,"title":87,"description":88,"category":89,"tags":90},"\u002Fposts\u002Fjs-encoding-today","現在處理編碼：TextEncoder、Uint8Array 的 Base64 方法與 URLSearchParams","整理 JavaScript 現代編碼方式，使用 TextEncoder、TextDecoder 與 Uint8Array 處理 UTF-8 與 Base64，並說明 Base64URL、URLSearchParams 與 encodeURIComponent 的差異。","research",[91,92,93,94,95,18],"編碼","TextEncoder","Base64","URLSearchParams","Uint8Array",{"path":97,"title":98,"description":99,"category":16,"tags":100},"\u002Fposts\u002Fdart-sass-migration","Dart Sass 遷移筆記：node-sass 之後怎麼裝 Sass","node-sass 與 LibSass 已停止維護，現在改用 Dart Sass。整理 webpack 與 Vite 的安裝方式，以及為什麼不再像 node-sass 一樣容易遇到 Node.js 版本與原生編譯問題。",[101,102,103],"Sass","Webpack","Vite",{"path":105,"title":106,"description":107,"category":16,"tags":108},"\u002Fposts\u002Fdayjs-vue3-computed","day.js 搭配 Vue 3 的日期格式化","整理 Vue 3 日期格式化的常見做法，使用 day.js 取代舊專案常見的 moment.js，並以 method 或 computed 取代 Vue 2 filter，也補充原生 Intl.DateTimeFormat 的使用方式。",[44,109,110,111],"day.js","日期","Intl",{"path":113,"title":114,"description":115,"category":16,"tags":116},"\u002Fposts\u002Fes-modules-today","ES Module 現在的樣子：Node ESM、exports 與 import maps","整理現代 JavaScript ES Module 的常見用法，包括 Node.js 原生 ESM、package.json exports、瀏覽器 import maps、JSON import attributes，以及 Vite 動態載入的注意事項。",[117,118,119,120,103,18],"ES Module","Node.js","import maps","exports",{"path":122,"title":123,"description":124,"category":89,"tags":125},"\u002Fposts\u002Fstructured-clone","structuredClone()：終於有內建的深層拷貝","整理 JavaScript structuredClone() 的深層拷貝用法，說明 Date、Map、Set、循環參考的處理方式，以及 Function、class 與 Vue reactive Proxy 的注意事項。",[18,126,127,128],"structuredClone","深拷貝","Vue",{"path":130,"title":131,"description":132,"category":16,"tags":133},"\u002Fposts\u002Fdynamic-import","JavaScript 動態載入：import() 與 Vue 非同步元件","整理靜態 import、動態 import() 與 Vue defineAsyncComponent 的使用方式，並簡單補充 Vite import.meta.glob 的用途。",[18,134,135,103,136],"ES Modules","Vue 3","Code Splitting",{"path":138,"title":139,"description":140,"category":16,"tags":141},"\u002Fposts\u002Fscreen-wake-lock-api","Screen Wake Lock API：不用套件也能讓頁面保持亮著","整理 Screen Wake Lock API 的基本用法，說明 HTTPS、頁面可見性、切換分頁後重新取得 wake lock，以及 Vue／Nuxt 計時器使用時的注意事項。",[142,128,143,144,145,146],"Wake Lock","Nuxt4","行動裝置","計時器","Web API",{"path":148,"title":149,"description":150,"category":16,"tags":151},"\u002Fposts\u002Fhtml-meta-boilerplate-today","現在的 HTML meta 標籤怎麼寫","整理現在一個新頁面該有的 meta 標籤骨架——viewport 補上 viewport-fit=cover 因應瀏海與圓角螢幕、color-scheme 讓表單控制項跟著深色模式換色、theme-color 決定行動瀏覽器工具列顏色，以及基本的 SEO 與 Open Graph 標籤。",[152,153,154],"HTML","meta","SEO",{"path":156,"title":157,"description":158,"category":8,"tags":159},"\u002Fposts\u002Fdocker-node-postgres","把 Node + PostgreSQL 的 backend 裝進 Docker：Dockerfile、.dockerignore 與 compose","從「本機裝了就跑」到打包成容器的通用做法——Dockerfile 每一行的理由與 layer cache 為什麼決定了指令順序、.dockerignore 漏掉 node_modules 對原生模組是災難級的問題、容器之間為什麼不能用 localhost 互相找到對方，以及怎麼驗證資料真的寫進了 volume。",[160,161,118,162,163],"Docker","Docker Compose","PostgreSQL","Express",{"path":165,"title":166,"description":167,"category":16,"tags":168},"\u002Fposts\u002Fnuxt-server-proxy","Nuxt 3 \u002F 4 用 Server API 做代理與轉發","整理 Nuxt 3 與 Nuxt 4 使用 server\u002Fapi 轉發第三方 API 的方式，可以避免瀏覽器 CORS 問題、隱藏 API Key，也能在伺服器端加入快取。",[169,143,170,171,172],"Nuxt3","CORS","Nitro","API",{"path":174,"title":175,"description":176,"category":16,"tags":177},"\u002Fposts\u002Fnuxt4-project-setup","Nuxt 4 建立專案：和 Nuxt 3 有哪些不同","整理 Nuxt 4 建立專案的基本流程與目錄結構，並對照 Nuxt 3 的差異，包括 app\u002F 目錄、路徑別名、TypeScript，以及目前常用的 ESLint 與 Tailwind CSS 4 設定。",[143,44,178,179,180],"TypeScript","ESLint","Tailwind CSS 4",{"path":182,"title":183,"description":184,"category":16,"tags":185},"\u002Fposts\u002Fnuxt3-route-middleware","Nuxt 3 \u002F 4 的 Route Middleware：進頁面前先做判斷","整理 Nuxt 3 與 Nuxt 4 的 Route Middleware，用 defineNuxtRouteMiddleware 在進入頁面前檢查登入狀態，搭配 navigateTo 重新導向，並說明一般 middleware、global middleware 與 definePageMeta 的用法。",[169,143,186,187],"Middleware","驗證",{"path":189,"title":190,"description":191,"category":16,"tags":192},"\u002Fposts\u002Fnuxt-route-rules-redirect","Nuxt 3 \u002F 4 的 routeRules：重新導向與頁面設定","整理 Nuxt 3 與 Nuxt 4 的 routeRules 用法，可以依照不同網址設定重新導向、預渲染、SSR 與快取，並簡單對照 Nuxt 2 的舊做法。",[169,143,193,194],"routeRules","重新導向",{"path":196,"title":197,"description":198,"category":16,"tags":199},"\u002Fposts\u002Fios-iframe-google-login-blank-page","iOS iframe 串接 Google 登入出現空白頁：Popup 與頁面回收問題排查紀錄","記錄 iframe 內串接 Google 第三方登入時，部分 iOS 裝置因背景頁面遭到回收並重新載入，導致 Popup 登入流程中斷，以及最後透過 Redirect 分流與登入狀態接回機制處理的過程。",[73,200,201,202,203,204,205,206],"Safari","iframe","Google Login","GIS","OAuth","Popup","Redirect",{"path":208,"title":209,"description":210,"category":16,"tags":211},"\u002Fposts\u002Fios-login-tab-return-behavior","iOS 第三方登入後回不到原頁？分頁開啟關係與回頁行為實測","記錄 iOS Safari 與 Chrome 在多分頁第三方登入流程中，登入頁關閉後不一定回到原頁的實測，以及分頁開啟關係、window.close 與 window.focus 的限制。",[73,200,212,202,204,201,213,214],"Chrome","window.close","window.focus",{"path":216,"title":217,"description":218,"category":16,"tags":219},"\u002Fposts\u002Fgoogle-app-about-blank-placeholder","Google App 內建瀏覽器：about:blank 佔位頁被關閉的排查紀錄","記錄 iOS Google App 內建瀏覽器在非同步開啟外部頁面時，about:blank 佔位分頁被自動關閉，導致第三方登入或外部導轉失敗的排查與解法。",[73,220,221,222,204,223],"Google App","window.open","about:blank","WebView",{"path":225,"title":226,"description":227,"category":228,"tags":229},"\u002Fposts\u002Fclaude-code-codex-cli-init-basics","AI 相關紀錄 v1｜Claude Code \u002F Codex CLI 初始化與基本概念","從 Claude Code 與 Codex CLI 開始，整理 CLI、Coding Agent、Context、\u002Finit 與 CLAUDE.md／AGENTS.md 這幾個一開始最容易搞混的概念，以及在既有專案中的安裝與啟動方式。","ai",[230,231,232,233,234,235],"AI","Claude Code","Codex CLI","Coding Agent","CLAUDE.md","AGENTS.md",{"path":237,"title":238,"description":239,"category":228,"tags":240},"\u002Fposts\u002Fclaude-agents-md-project-guidelines","AI 相關紀錄 v2｜CLAUDE.md \u002F AGENTS.md 怎麼寫？專案規範與實際範例","整理 CLAUDE.md 與 AGENTS.md 的用途、適合記錄的專案規範，以及可直接參考的設定範例。",[230,231,232,234,235],{"path":242,"title":243,"description":244,"category":228,"tags":245},"\u002Fposts\u002Fclaude-codex-config-locations","AI 相關紀錄 v3｜Claude Code `.claude\u002F` \u002F Codex CLI 設定位置：全域與專案設定基本概要介紹","整理 Claude Code 與 Codex CLI 的使用者層級、專案層級設定位置，以及 rules、skills、agents 等目錄的用途。",[230,231,232,246],"設定檔",{"path":248,"title":249,"description":250,"category":228,"tags":251},"\u002Fposts\u002Fclaude-settings-codex-config","AI 相關紀錄 v4｜Claude Code settings \u002F Codex config 基本設定","整理 Claude Code settings.json 與 Codex config.toml 的設定層級、權限控制和常見基本設定。",[230,231,232,252,253],"settings.json","config.toml",{"path":255,"title":256,"description":257,"category":228,"tags":258},"\u002Fposts\u002Fclaude-codex-rules-structure","AI 相關紀錄 v5｜Rules：專案規則多了之後怎麼拆？","整理 Claude Code Rules 與 Codex AGENTS.md 的拆分方式，說明何時該拆、怎麼依目錄管理專案規則。",[230,231,232,259,235],"Rules",{"path":261,"title":262,"description":263,"category":228,"tags":264},"\u002Fposts\u002Fclaude-codex-slash-commands","AI 相關紀錄 v6｜Commands \u002F Slash Commands：CLI 裡的 `\u002F` 指令是什麼？","介紹 Claude Code 與 Codex CLI 的 Slash Commands，整理常用內建指令、自訂指令與 Skills 的差異。",[230,231,232,265],"Slash Commands",{"path":267,"title":268,"description":269,"category":228,"tags":270},"\u002Fposts\u002Fclaude-codex-skills","AI 相關紀錄 v7｜Skills：把重複工作流程整理起來","介紹 Claude Code 與 Codex Skills，整理適合保存成 Skill 的重複流程、基本結構與使用方式。",[230,231,232,271],"Skills",{"path":273,"title":274,"description":275,"category":228,"tags":276},"\u002Fposts\u002Fclaude-codex-agents-subagents","AI 相關紀錄 v8｜Agent \u002F Subagent：什麼時候真的需要分工？","整理 Agent、Subagent 與 Custom Agent 的用途，說明何時適合分工，以及它們和 Rules、Skills 的差異。",[230,231,232,277,278],"Agent","Subagent",{"path":280,"title":281,"description":282,"category":228,"tags":283},"\u002Fposts\u002Fmcp-basics","AI 相關紀錄 v9｜MCP：讓 AI 使用其他工具與資料","介紹 MCP 如何讓 AI 使用外部工具與資料，並以 Pencil MCP 說明實際用途、權限風險及與 Skill 的差異。",[230,284,231,232,285],"MCP","Pencil",{"path":287,"title":288,"description":289,"category":228,"tags":290},"\u002Fposts\u002Fai-coding-agent-concepts-summary","AI 相關紀錄 v10｜基本概念小結論","用一篇快速整理 CLAUDE.md、AGENTS.md、設定檔、Rules、Commands、Skills、Agent 與 MCP 的角色和差異。",[230,231,232,284,271,277],{"path":292,"title":293,"description":294,"category":228,"tags":295},"\u002Fposts\u002Fclaude-code-codex-model-selection","Claude Code 與 Codex：不同模型怎麼選？","比較 Claude Code 與 Codex 不同模型的速度、能力與成本取捨，依工作難度整理實用的選擇方向。",[230,231,296,297],"Codex","模型選擇",{"path":299,"title":300,"description":301,"category":228,"tags":302},"\u002Fposts\u002Fclaude-model-selection","Claude 模型簡單怎麼選？","整理 Claude 各模型在速度、能力與使用情境上的差異，提供從輕量工作到複雜任務的選擇方向。",[230,231,303,297],"Claude",{"path":305,"title":306,"description":307,"category":228,"tags":308},"\u002Fposts\u002Fcodex-model-selection","Codex 模型簡單怎麼選？","整理 Codex 各模型在速度、能力與使用情境上的差異，提供從簡單修改到大型端到端任務的選擇方向。",[230,296,309,297],"OpenAI",{"path":311,"title":312,"description":313,"category":228,"tags":314},"\u002Fposts\u002Fclaude-code-codex-cli-command-cheatsheet","Claude Code \u002F Codex CLI 常用指令速查表","整理 Claude Code 與 Codex CLI 的常用斜線指令、快捷操作與進階功能，方便需要時快速查找。",[230,231,232,265,315],"速查表",{"id":317,"title":79,"body":318,"category":16,"description":80,"draft":1357,"extension":1358,"image":1359,"meta":1360,"navigation":1005,"path":78,"publishedAt":1361,"relatedExperiments":1362,"seo":1363,"stem":1364,"tags":1365,"updatedAt":1366,"__hash__":1367},"posts\u002Fposts\u002F20250808.playwright-vs-cypress.md",{"type":319,"value":320,"toc":1341},"minimark",[321,344,348,352,362,377,383,394,397,400,513,517,522,529,536,662,665,669,676,720,723,727,730,777,784,798,804,807,810,815,975,980,1150,1164,1167,1183,1186,1190,1193,1231,1234,1238,1241,1267,1270,1302,1305,1337],[322,323,324,333,336],"ul",{},[325,326,327,328,332],"li",{},"幾年前做端對端測試時，Cypress 是很常見的選擇，互動式 Test Runner、",[329,330,331],"code",{},"npx cypress open"," 與鏈式 API 都很適合入門。",[325,334,335],{},"這幾年 Playwright 起來得很快，現在多數新專案會直接選它。",[325,337,338,339,343],{},"這篇講兩者的實際差異，以及什麼情況下 Cypress 還是好選擇——",[340,341,342],"strong",{},"這不是「Cypress 已死」的文章","，它仍然是個好工具。",[345,346,347],"h2",{"id":347},"架構上的根本差異",[349,350,351],"p",{},"兩者最大的不同在測試程式跑在哪裡。",[349,353,354,357,358,361],{},[340,355,356],{},"Cypress 把測試程式跑在瀏覽器裡","，跟被測試的應用共用同一個執行環境。這帶來很好的除錯體驗（可以直接 ",[329,359,360],{},"debugger","、在 DevTools 裡看），但也帶來限制：",[322,363,364,367,374],{},[325,365,366],{},"難以跨分頁、跨網域操作",[325,368,369,370,373],{},"沒辦法直接碰檔案系統或發原生的 HTTP 請求（要透過 ",[329,371,372],{},"cy.task"," 跳到 Node 端）",[325,375,376],{},"受同源政策約束",[349,378,379,382],{},[340,380,381],{},"Playwright 把測試程式跑在 Node 裡","，透過 CDP（Chrome DevTools Protocol）之類的協定遠端驅動瀏覽器。\n測試程式與應用完全分離，所以：",[322,384,385,388,391],{},[325,386,387],{},"多分頁、多網域、多使用者情境都是原生支援",[325,389,390],{},"可以隨意讀寫檔案、呼叫 API、操作資料庫來準備測試資料",[325,392,393],{},"一份測試可以同時跑在 Chromium、Firefox、WebKit",[349,395,396],{},"這個架構差異解釋了後面幾乎所有的功能差別。",[345,398,399],{"id":399},"主要差異",[401,402,403,417],"table",{},[404,405,406],"thead",{},[407,408,409,413,415],"tr",{},[410,411,412],"th",{},"項目",[410,414,82],{},[410,416,76],{},[418,419,420,432,446,459,470,481,491,502],"tbody",{},[407,421,422,426,429],{},[423,424,425],"td",{},"測試程式執行位置",[423,427,428],{},"瀏覽器內",[423,430,431],{},"Node 端",[407,433,434,437,440],{},[423,435,436],{},"瀏覽器引擎",[423,438,439],{},"Chromium、Firefox、Electron",[423,441,442,443],{},"Chromium、Firefox、",[340,444,445],{},"WebKit",[407,447,448,451,454],{},[423,449,450],{},"平行執行",[423,452,453],{},"需付費的 Cypress Cloud",[423,455,456],{},[340,457,458],{},"內建免費",[407,460,461,464,467],{},[423,462,463],{},"多分頁 \u002F 多網域",[423,465,466],{},"受限",[423,468,469],{},"原生支援",[407,471,472,475,478],{},[423,473,474],{},"語言",[423,476,477],{},"JavaScript \u002F TypeScript",[423,479,480],{},"TS\u002FJS、Python、Java、.NET",[407,482,483,486,489],{},[423,484,485],{},"自動等待",[423,487,488],{},"有",[423,490,488],{},[407,492,493,496,499],{},[423,494,495],{},"除錯工具",[423,497,498],{},"互動式 Test Runner",[423,500,501],{},"Trace Viewer、UI Mode、Codegen",[407,503,504,507,510],{},[423,505,506],{},"授權",[423,508,509],{},"MIT（部分功能需付費雲端）",[423,511,512],{},"MIT，全部功能免費",[345,514,516],{"id":515},"playwright-的三個關鍵優勢","Playwright 的三個關鍵優勢",[518,519,521],"h3",{"id":520},"_1-真正的-webkit-測試","1. 真正的 WebKit 測試",[349,523,524,525,528],{},"Cypress 沒有 WebKit 支援（曾有實驗性支援但不完整）。這代表 ",[340,526,527],{},"Safari 專屬的 bug 測不到","——而那正是最常出問題的地方。",[349,530,531,532,535],{},"Playwright 的 ",[329,533,534],{},"webkit"," 用的是真正的 WebKit 引擎。雖然仍不完全等於 iOS 上的 Safari（沒有 iOS 的系統整合），但比完全測不到好太多。",[537,538,543],"pre",{"className":539,"code":540,"language":541,"meta":542,"style":542},"language-javascript shiki shiki-themes github-light github-dark-default","\u002F\u002F playwright.config.ts\nexport default defineConfig({\n  projects: [\n    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },\n    { name: 'firefox',  use: { ...devices['Desktop Firefox'] } },\n    { name: 'webkit',   use: { ...devices['Desktop Safari'] } },\n  ],\n});\n","javascript","",[329,544,545,554,572,579,604,624,644,653],{"__ignoreMap":542},[546,547,550],"span",{"class":548,"line":549},"line",1,[546,551,553],{"class":552},"sPyeA","\u002F\u002F playwright.config.ts\n",[546,555,557,561,564,568],{"class":548,"line":556},2,[546,558,560],{"class":559},"sDgYj","export",[546,562,563],{"class":559}," default",[546,565,567],{"class":566},"sVjZL"," defineConfig",[546,569,571],{"class":570},"sfpNf","({\n",[546,573,575],{"class":548,"line":574},3,[546,576,578],{"class":577},"szmH1","  projects: [\n",[546,580,582,585,589,592,595,598,601],{"class":548,"line":581},4,[546,583,584],{"class":577},"    { name: ",[546,586,588],{"class":587},"sNbzA","'chromium'",[546,590,591],{"class":577},", use: { ",[546,593,594],{"class":559},"...",[546,596,597],{"class":577},"devices[",[546,599,600],{"class":587},"'Desktop Chrome'",[546,602,603],{"class":577},"] } },\n",[546,605,607,609,612,615,617,619,622],{"class":548,"line":606},5,[546,608,584],{"class":577},[546,610,611],{"class":587},"'firefox'",[546,613,614],{"class":577},",  use: { ",[546,616,594],{"class":559},[546,618,597],{"class":577},[546,620,621],{"class":587},"'Desktop Firefox'",[546,623,603],{"class":577},[546,625,627,629,632,635,637,639,642],{"class":548,"line":626},6,[546,628,584],{"class":577},[546,630,631],{"class":587},"'webkit'",[546,633,634],{"class":577},",   use: { ",[546,636,594],{"class":559},[546,638,597],{"class":577},[546,640,641],{"class":587},"'Desktop Safari'",[546,643,603],{"class":577},[546,645,647,650],{"class":548,"line":646},7,[546,648,649],{"class":577},"  ]",[546,651,652],{"class":570},",\n",[546,654,656,659],{"class":548,"line":655},8,[546,657,658],{"class":570},"})",[546,660,661],{"class":577},";\n",[349,663,664],{},"同一份測試自動在三個引擎上跑。",[518,666,668],{"id":667},"_2-平行執行免費","2. 平行執行免費",[349,670,671,672,675],{},"Cypress 的平行執行需要 Cypress Cloud（付費）。Playwright 的 ",[329,673,674],{},"workers"," 是內建的：",[537,677,679],{"className":539,"code":678,"language":541,"meta":542,"style":542},"export default defineConfig({\n  workers: process.env.CI ? 4 : undefined,\n});\n",[329,680,681,691,714],{"__ignoreMap":542},[546,682,683,685,687,689],{"class":548,"line":549},[546,684,560],{"class":559},[546,686,563],{"class":559},[546,688,567],{"class":566},[546,690,571],{"class":570},[546,692,693,696,700,703,706,709,712],{"class":548,"line":556},[546,694,695],{"class":577},"  workers: process.env.",[546,697,699],{"class":698},"sPgVT","CI",[546,701,702],{"class":559}," ?",[546,704,705],{"class":698}," 4",[546,707,708],{"class":559}," :",[546,710,711],{"class":698}," undefined",[546,713,652],{"class":570},[546,715,716,718],{"class":548,"line":574},[546,717,658],{"class":570},[546,719,661],{"class":577},[349,721,722],{},"E2E 測試最大的痛點就是慢，一套跑 20 分鐘的測試沒有人願意在每次 PR 都等。平行執行是不是要另外付費，實務上影響很大。",[518,724,726],{"id":725},"_3-trace-viewer","3. Trace Viewer",[349,728,729],{},"這是我覺得最有價值的功能。測試失敗時，Playwright 可以錄下完整的執行軌跡：",[537,731,733],{"className":539,"code":732,"language":541,"meta":542,"style":542},"export default defineConfig({\n  use: {\n    trace: 'on-first-retry',   \u002F\u002F 第一次重試時錄\n  },\n});\n",[329,734,735,745,750,764,771],{"__ignoreMap":542},[546,736,737,739,741,743],{"class":548,"line":549},[546,738,560],{"class":559},[546,740,563],{"class":559},[546,742,567],{"class":566},[546,744,571],{"class":570},[546,746,747],{"class":548,"line":556},[546,748,749],{"class":577},"  use: {\n",[546,751,752,755,758,761],{"class":548,"line":574},[546,753,754],{"class":577},"    trace: ",[546,756,757],{"class":587},"'on-first-retry'",[546,759,760],{"class":577},",   ",[546,762,763],{"class":552},"\u002F\u002F 第一次重試時錄\n",[546,765,766,769],{"class":548,"line":581},[546,767,768],{"class":577},"  }",[546,770,652],{"class":570},[546,772,773,775],{"class":548,"line":606},[546,774,658],{"class":570},[546,776,661],{"class":577},[349,778,779,780,783],{},"失敗後用 ",[329,781,782],{},"npx playwright show-trace trace.zip"," 打開，會看到：",[322,785,786,789,792,795],{},[325,787,788],{},"每一步操作的畫面快照，可以來回拖曳",[325,790,791],{},"每一步的 DOM 狀態（可以用 DevTools 檢查）",[325,793,794],{},"網路請求、Console log",[325,796,797],{},"完整的操作時間軸",[349,799,800,803],{},[340,801,802],{},"CI 上偶發失敗的測試最難查","，因為你重跑一次就好了。有 trace 之後，那次失敗當下的完整現場都留著。這件事 Cypress 的影片錄製做不到同樣的深度。",[345,805,806],{"id":806},"寫法對照",[349,808,809],{},"概念幾乎一樣，語法有些差異。",[349,811,812],{},[340,813,814],{},"Cypress：",[537,816,818],{"className":539,"code":817,"language":541,"meta":542,"style":542},"describe('登入', () => {\n  it('用正確帳密可以登入', () => {\n    cy.visit('\u002Flogin');\n    cy.get('[data-testid=email]').type('user@example.com');\n    cy.get('[data-testid=password]').type('password');\n    cy.get('button[type=submit]').click();\n    cy.url().should('include', '\u002Fdashboard');\n  });\n});\n",[329,819,820,840,856,872,897,919,938,964,969],{"__ignoreMap":542},[546,821,822,825,828,831,834,837],{"class":548,"line":549},[546,823,824],{"class":566},"describe",[546,826,827],{"class":577},"(",[546,829,830],{"class":587},"'登入'",[546,832,833],{"class":577},", () ",[546,835,836],{"class":559},"=>",[546,838,839],{"class":577}," {\n",[546,841,842,845,847,850,852,854],{"class":548,"line":556},[546,843,844],{"class":566},"  it",[546,846,827],{"class":577},[546,848,849],{"class":587},"'用正確帳密可以登入'",[546,851,833],{"class":577},[546,853,836],{"class":559},[546,855,839],{"class":577},[546,857,858,861,864,866,869],{"class":548,"line":574},[546,859,860],{"class":577},"    cy.",[546,862,863],{"class":566},"visit",[546,865,827],{"class":577},[546,867,868],{"class":587},"'\u002Flogin'",[546,870,871],{"class":577},");\n",[546,873,874,876,879,881,884,887,890,892,895],{"class":548,"line":581},[546,875,860],{"class":577},[546,877,878],{"class":566},"get",[546,880,827],{"class":577},[546,882,883],{"class":587},"'[data-testid=email]'",[546,885,886],{"class":577},").",[546,888,889],{"class":566},"type",[546,891,827],{"class":577},[546,893,894],{"class":587},"'user@example.com'",[546,896,871],{"class":577},[546,898,899,901,903,905,908,910,912,914,917],{"class":548,"line":606},[546,900,860],{"class":577},[546,902,878],{"class":566},[546,904,827],{"class":577},[546,906,907],{"class":587},"'[data-testid=password]'",[546,909,886],{"class":577},[546,911,889],{"class":566},[546,913,827],{"class":577},[546,915,916],{"class":587},"'password'",[546,918,871],{"class":577},[546,920,921,923,925,927,930,932,935],{"class":548,"line":626},[546,922,860],{"class":577},[546,924,878],{"class":566},[546,926,827],{"class":577},[546,928,929],{"class":587},"'button[type=submit]'",[546,931,886],{"class":577},[546,933,934],{"class":566},"click",[546,936,937],{"class":577},"();\n",[546,939,940,942,945,948,951,953,956,959,962],{"class":548,"line":646},[546,941,860],{"class":577},[546,943,944],{"class":566},"url",[546,946,947],{"class":577},"().",[546,949,950],{"class":566},"should",[546,952,827],{"class":577},[546,954,955],{"class":587},"'include'",[546,957,958],{"class":577},", ",[546,960,961],{"class":587},"'\u002Fdashboard'",[546,963,871],{"class":577},[546,965,966],{"class":548,"line":655},[546,967,968],{"class":577},"  });\n",[546,970,972],{"class":548,"line":971},9,[546,973,974],{"class":577},"});\n",[349,976,977],{},[340,978,979],{},"Playwright：",[537,981,983],{"className":539,"code":982,"language":541,"meta":542,"style":542},"import { test, expect } from '@playwright\u002Ftest';\n\ntest('用正確帳密可以登入', async ({ page }) => {\n  await page.goto('\u002Flogin');\n  await page.getByLabel('Email').fill('user@example.com');\n  await page.getByLabel('密碼').fill('password');\n  await page.getByRole('button', { name: '登入' }).click();\n  await expect(page).toHaveURL(\u002Fdashboard\u002F);\n});\n",[329,984,985,1001,1007,1035,1052,1077,1100,1126,1146],{"__ignoreMap":542},[546,986,987,990,993,996,999],{"class":548,"line":549},[546,988,989],{"class":559},"import",[546,991,992],{"class":577}," { test, expect } ",[546,994,995],{"class":559},"from",[546,997,998],{"class":587}," '@playwright\u002Ftest'",[546,1000,661],{"class":577},[546,1002,1003],{"class":548,"line":556},[546,1004,1006],{"emptyLinePlaceholder":1005},true,"\n",[546,1008,1009,1012,1014,1016,1018,1021,1024,1028,1031,1033],{"class":548,"line":574},[546,1010,1011],{"class":566},"test",[546,1013,827],{"class":577},[546,1015,849],{"class":587},[546,1017,958],{"class":577},[546,1019,1020],{"class":559},"async",[546,1022,1023],{"class":577}," ({ ",[546,1025,1027],{"class":1026},"sjlk-","page",[546,1029,1030],{"class":577}," }) ",[546,1032,836],{"class":559},[546,1034,839],{"class":577},[546,1036,1037,1040,1043,1046,1048,1050],{"class":548,"line":581},[546,1038,1039],{"class":559},"  await",[546,1041,1042],{"class":577}," page.",[546,1044,1045],{"class":566},"goto",[546,1047,827],{"class":577},[546,1049,868],{"class":587},[546,1051,871],{"class":577},[546,1053,1054,1056,1058,1061,1063,1066,1068,1071,1073,1075],{"class":548,"line":606},[546,1055,1039],{"class":559},[546,1057,1042],{"class":577},[546,1059,1060],{"class":566},"getByLabel",[546,1062,827],{"class":577},[546,1064,1065],{"class":587},"'Email'",[546,1067,886],{"class":577},[546,1069,1070],{"class":566},"fill",[546,1072,827],{"class":577},[546,1074,894],{"class":587},[546,1076,871],{"class":577},[546,1078,1079,1081,1083,1085,1087,1090,1092,1094,1096,1098],{"class":548,"line":626},[546,1080,1039],{"class":559},[546,1082,1042],{"class":577},[546,1084,1060],{"class":566},[546,1086,827],{"class":577},[546,1088,1089],{"class":587},"'密碼'",[546,1091,886],{"class":577},[546,1093,1070],{"class":566},[546,1095,827],{"class":577},[546,1097,916],{"class":587},[546,1099,871],{"class":577},[546,1101,1102,1104,1106,1109,1111,1114,1117,1119,1122,1124],{"class":548,"line":646},[546,1103,1039],{"class":559},[546,1105,1042],{"class":577},[546,1107,1108],{"class":566},"getByRole",[546,1110,827],{"class":577},[546,1112,1113],{"class":587},"'button'",[546,1115,1116],{"class":577},", { name: ",[546,1118,830],{"class":587},[546,1120,1121],{"class":577}," }).",[546,1123,934],{"class":566},[546,1125,937],{"class":577},[546,1127,1128,1130,1133,1136,1139,1141,1144],{"class":548,"line":655},[546,1129,1039],{"class":559},[546,1131,1132],{"class":566}," expect",[546,1134,1135],{"class":577},"(page).",[546,1137,1138],{"class":566},"toHaveURL",[546,1140,827],{"class":577},[546,1142,1143],{"class":587},"\u002Fdashboard\u002F",[546,1145,871],{"class":577},[546,1147,1148],{"class":548,"line":971},[546,1149,974],{"class":577},[349,1151,1152,1153,1159,1160,1163],{},"最明顯的差別是 ",[340,1154,1155,1156],{},"Playwright 用 ",[329,1157,1158],{},"await","，而 Cypress 用鏈式的隱含佇列。Cypress 的寫法看起來比較短，但那個「看起來像同步、其實不是」的特性也是新手最常困惑的地方——在 Cypress 裡寫 ",[329,1161,1162],{},"const el = cy.get(...)"," 拿不到元素。",[518,1165,1166],{"id":1166},"建議用語意化的定位器",[349,1168,1169,1171,1172,1174,1175,1178,1179,1182],{},[329,1170,1108],{},"、",[329,1173,1060],{}," 這類定位器比 CSS selector 好，理由是",[340,1176,1177],{},"它們同時在測無障礙","：如果 ",[329,1180,1181],{},"getByRole('button', { name: '登入' })"," 找不到東西，通常代表那顆按鈕對螢幕閱讀器來說也是有問題的。",[349,1184,1185],{},"Cypress 也支援 Testing Library 的同類定位器，這點兩邊都做得到。",[345,1187,1189],{"id":1188},"從-cypress-遷移","從 Cypress 遷移",[349,1191,1192],{},"有幾件事要有心理準備：",[322,1194,1195,1207,1217],{},[325,1196,1197,1206],{},[340,1198,1199,1202,1203],{},[329,1200,1201],{},"cy.intercept"," 對應 ",[329,1204,1205],{},"page.route","，API 略有不同",[325,1208,1209,1212,1213,1216],{},[340,1210,1211],{},"自訂 command 要改寫成 fixture","，Playwright 的擴充機制是 fixture 而不是掛在 ",[329,1214,1215],{},"cy"," 上",[325,1218,1219,1226,1227,1230],{},[340,1220,1221,1222,1225],{},"Cypress 的隱含等待與重試邏輯要改成 ",[329,1223,1224],{},"expect"," 的 web-first assertion","：",[329,1228,1229],{},"await expect(locator).toBeVisible()"," 會自動重試，這是 Playwright 處理非同步的核心機制",[349,1232,1233],{},"不建議一次全部搬。實務上的做法是新測試用 Playwright 寫，舊的 Cypress 測試繼續跑，等哪天舊測試需要大改時再順手轉。",[345,1235,1237],{"id":1236},"cypress-什麼時候還是好選擇","Cypress 什麼時候還是好選擇",[349,1239,1240],{},"講完 Playwright 的優勢，也要講清楚 Cypress 仍然有優勢的地方：",[322,1242,1243,1249,1255,1261],{},[325,1244,1245,1248],{},[340,1246,1247],{},"既有投資","：已經有幾百個 Cypress 測試而且跑得好好的，沒有理由為了換而換。",[325,1250,1251,1254],{},[340,1252,1253],{},"互動式除錯體驗","：Cypress 的 Test Runner 讓你在測試暫停的狀態下直接操作頁面，這個體驗仍然很好。",[325,1256,1257,1260],{},[340,1258,1259],{},"Component Testing","：兩邊都有，但 Cypress 的元件測試整合度較成熟。",[325,1262,1263,1266],{},[340,1264,1265],{},"團隊熟悉度","：工具再好，團隊不會用就是負擔。",[345,1268,1269],{"id":1269},"該用哪一個",[322,1271,1272,1278,1284,1290,1296],{},[325,1273,1274,1277],{},[340,1275,1276],{},"新專案"," → Playwright。免費的平行執行、WebKit 支援與 trace viewer 是實打實的差距。",[325,1279,1280,1283],{},[340,1281,1282],{},"既有 Cypress 專案跑得好"," → 不用急著換。",[325,1285,1286,1289],{},[340,1287,1288],{},"需要測 Safari 行為"," → Playwright，這點沒有替代方案。",[325,1291,1292,1295],{},[340,1293,1294],{},"需要多分頁、多使用者情境","（聊天室、協作編輯） → Playwright。",[325,1297,1298,1301],{},[340,1299,1300],{},"主要做元件測試"," → 兩邊都行，Cypress 略成熟。",[345,1303,1304],{"id":1304},"參考資料",[322,1306,1307,1316,1323,1330],{},[325,1308,1309],{},[1310,1311,1315],"a",{"href":1312,"rel":1313},"https:\u002F\u002Fplaywright.dev\u002F",[1314],"nofollow","Playwright 官方文件",[325,1317,1318],{},[1310,1319,1322],{"href":1320,"rel":1321},"https:\u002F\u002Fplaywright.dev\u002Fdocs\u002Ftrace-viewer",[1314],"Playwright Trace Viewer",[325,1324,1325],{},[1310,1326,1329],{"href":1327,"rel":1328},"https:\u002F\u002Fplaywright.dev\u002Fdocs\u002Flocators",[1314],"Playwright 定位器",[325,1331,1332],{},[1310,1333,1336],{"href":1334,"rel":1335},"https:\u002F\u002Fdocs.cypress.io\u002F",[1314],"Cypress 官方文件",[1338,1339,1340],"style",{},"html pre.shiki code .sPyeA, html code.shiki .sPyeA{--shiki-default:#6A737D;--shiki-dark:#8B949E}html pre.shiki code .sDgYj, html code.shiki .sDgYj{--shiki-default:#D73A49;--shiki-dark:#FF7B72}html pre.shiki code .sVjZL, html code.shiki .sVjZL{--shiki-default:#6F42C1;--shiki-dark:#D2A8FF}html pre.shiki code .sfpNf, html code.shiki .sfpNf{--shiki-default:#24292E;--shiki-dark:#FFA657}html pre.shiki code .szmH1, html code.shiki .szmH1{--shiki-default:#24292E;--shiki-dark:#E6EDF3}html pre.shiki code .sNbzA, html code.shiki .sNbzA{--shiki-default:#032F62;--shiki-dark:#A5D6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sPgVT, html code.shiki .sPgVT{--shiki-default:#005CC5;--shiki-dark:#79C0FF}html pre.shiki code .sjlk-, html code.shiki .sjlk-{--shiki-default:#E36209;--shiki-dark:#FFA657}",{"title":542,"searchDepth":574,"depth":574,"links":1342},[1343,1344,1345,1350,1353,1354,1355,1356],{"id":347,"depth":556,"text":347},{"id":399,"depth":556,"text":399},{"id":515,"depth":556,"text":516,"children":1346},[1347,1348,1349],{"id":520,"depth":574,"text":521},{"id":667,"depth":574,"text":668},{"id":725,"depth":574,"text":726},{"id":806,"depth":556,"text":806,"children":1351},[1352],{"id":1166,"depth":574,"text":1166},{"id":1188,"depth":556,"text":1189},{"id":1236,"depth":556,"text":1237},{"id":1269,"depth":556,"text":1269},{"id":1304,"depth":556,"text":1304},false,"md",null,{},"2025-08-08",[],{"title":79,"description":80},"posts\u002F20250808.playwright-vs-cypress",[76,82,83,84],"2026-09-08","K0DeKv2Wy4ThoYXxmjdQT2i1QyMvI2lON2ZRzQ9kvXM",[1369,1372],{"title":87,"path":86,"stem":1370,"description":88,"category":89,"publishedAt":1371,"children":-1},"posts\u002F20260325.js-encoding-today","2026-03-25",{"title":69,"path":68,"stem":1373,"description":70,"category":8,"publishedAt":1374,"children":-1},"posts\u002F20250725.mobile-debugging-today","2025-07-25",1791786901391]