[{"data":1,"prerenderedAt":1934},["ShallowReactive",2],{"navigation":3,"\u002Fdeployment\u002Fbrowser-tests":604,"\u002Fdeployment\u002Fbrowser-tests-surround":1929},[4,24,67,93,236,533],{"title":5,"path":6,"stem":7,"children":8,"collapsed":23},"Getting Started","\u002Fgetting-started","1.getting-started\u002F1.index",[9,11,15,19],{"title":10,"path":6,"stem":7},"Introduction",{"title":12,"path":13,"stem":14},"Installation","\u002Fgetting-started\u002Finstallation","1.getting-started\u002F2.installation",{"title":16,"path":17,"stem":18},"Key Ideas","\u002Fgetting-started\u002Fkey-ideas","1.getting-started\u002F3.key-ideas",{"title":20,"path":21,"stem":22},"Releases","\u002Fgetting-started\u002Freleases","1.getting-started\u002F4.releases",true,{"title":25,"path":26,"stem":27,"children":28,"collapsed":23},"Core Concepts","\u002Fcore-concepts","2.core-concepts\u002F0.index",[29,31,35,39,43,47,51,55,59,63],{"title":30,"path":26,"stem":27},"Overview",{"title":32,"path":33,"stem":34},"How It All Works","\u002Fcore-concepts\u002Farchitecture","2.core-concepts\u002F1.architecture",{"title":36,"path":37,"stem":38},"The Data Model","\u002Fcore-concepts\u002Fthe-data-model","2.core-concepts\u002F2.the-data-model",{"title":40,"path":41,"stem":42},"Layouts & Pages","\u002Fcore-concepts\u002Flayouts-and-pages","2.core-concepts\u002F3.layouts-and-pages",{"title":44,"path":45,"stem":46},"Dynamic Pages","\u002Fcore-concepts\u002Fdynamic-pages","2.core-concepts\u002F4.dynamic-pages",{"title":48,"path":49,"stem":50},"Components","\u002Fcore-concepts\u002Fcomponents","2.core-concepts\u002F5.components",{"title":52,"path":53,"stem":54},"Draft & Publish Workflow","\u002Fcore-concepts\u002Fdraft-and-publish","2.core-concepts\u002F6.draft-and-publish",{"title":56,"path":57,"stem":58},"The Admin Panel","\u002Fcore-concepts\u002Fadmin-panel","2.core-concepts\u002F7.admin-panel",{"title":60,"path":61,"stem":62},"Caching","\u002Fcore-concepts\u002Fcaching","2.core-concepts\u002F8.caching",{"title":64,"path":65,"stem":66},"Glossary","\u002Fcore-concepts\u002Fglossary","2.core-concepts\u002F9.glossary",{"title":68,"path":69,"stem":70,"children":71,"collapsed":23},"Guides","\u002Fguides","3.guides\u002F0.index",[72,73,77,81,85,89],{"title":30,"path":69,"stem":70},{"title":74,"path":75,"stem":76},"Your First Layout","\u002Fguides\u002Fyour-first-layout","3.guides\u002F1.your-first-layout",{"title":78,"path":79,"stem":80},"Your First Page Template","\u002Fguides\u002Fyour-first-page-template","3.guides\u002F2.your-first-page-template",{"title":82,"path":83,"stem":84},"Your First Component","\u002Fguides\u002Fyour-first-component","3.guides\u002F3.your-first-component",{"title":86,"path":87,"stem":88},"Alternative UI Variants","\u002Fguides\u002Falternative-ui-variants","3.guides\u002F4.alternative-ui-variants",{"title":90,"path":91,"stem":92},"Your First Dynamic Page","\u002Fguides\u002Fyour-first-dynamic-page","3.guides\u002F5.your-first-dynamic-page",{"title":94,"path":95,"stem":96,"children":97,"collapsed":23},"API Bundle (Back-End)","\u002Fapi","4.api\u002F0.index",[98,99,103,107,129,175,179,183,212,216,220,224,228,232],{"title":30,"path":95,"stem":96},{"title":100,"path":101,"stem":102},"Bundle Setup","\u002Fapi\u002Fbundle-setup","4.api\u002F02.bundle-setup",{"title":104,"path":105,"stem":106},"Configuration Reference","\u002Fapi\u002Fconfiguration","4.api\u002F03.configuration",{"title":108,"path":109,"stem":110,"children":111,"page":128},"Users","\u002Fapi\u002Fusers","4.api\u002F04.users",[112,116,120,124],{"title":113,"path":114,"stem":115},"Authentication","\u002Fapi\u002Fusers\u002Fauthentication","4.api\u002F04.users\u002F1.authentication",{"title":117,"path":118,"stem":119},"User Emails","\u002Fapi\u002Fusers\u002Fuser-emails","4.api\u002F04.users\u002F2.user-emails",{"title":121,"path":122,"stem":123},"Access Control","\u002Fapi\u002Fusers\u002Faccess-control","4.api\u002F04.users\u002F3.access-control",{"title":125,"path":126,"stem":127},"Sites with Non-Admin Users","\u002Fapi\u002Fusers\u002Fnon-admin-users","4.api\u002F04.users\u002F4.non-admin-users",false,{"title":48,"path":130,"stem":131,"children":132,"page":128},"\u002Fapi\u002Fcomponents","4.api\u002F05.components",[133,137,158,171],{"title":134,"path":135,"stem":136},"Creating Components","\u002Fapi\u002Fcomponents\u002Fcreating-components","4.api\u002F05.components\u002F1.creating-components",{"title":138,"path":139,"stem":140,"children":141,"page":128},"Annotations","\u002Fapi\u002Fcomponents\u002Fannotations","4.api\u002F05.components\u002F2.annotations",[142,146,150,154],{"title":143,"path":144,"stem":145},"Publishable","\u002Fapi\u002Fcomponents\u002Fannotations\u002Fpublishable","4.api\u002F05.components\u002F2.annotations\u002F1.publishable",{"title":147,"path":148,"stem":149},"Uploadable","\u002Fapi\u002Fcomponents\u002Fannotations\u002Fuploadable","4.api\u002F05.components\u002F2.annotations\u002F2.uploadable",{"title":151,"path":152,"stem":153},"Timestamped","\u002Fapi\u002Fcomponents\u002Fannotations\u002Ftimestamped","4.api\u002F05.components\u002F2.annotations\u002F3.timestamped",{"title":155,"path":156,"stem":157},"Explicit Allow Only","\u002Fapi\u002Fcomponents\u002Fannotations\u002Fexplicit-allow-only","4.api\u002F05.components\u002F2.annotations\u002F4.explicit-allow-only",{"title":159,"path":160,"stem":161,"children":162,"page":128},"Built Ins","\u002Fapi\u002Fcomponents\u002Fbuilt-ins","4.api\u002F05.components\u002F3.built-ins",[163,167],{"title":164,"path":165,"stem":166},"Collection Component","\u002Fapi\u002Fcomponents\u002Fbuilt-ins\u002Fcollection-component","4.api\u002F05.components\u002F3.built-ins\u002F1.collection-component",{"title":168,"path":169,"stem":170},"Form Component","\u002Fapi\u002Fcomponents\u002Fbuilt-ins\u002Fform-component","4.api\u002F05.components\u002F3.built-ins\u002F2.form-component",{"title":172,"path":173,"stem":174},"Groups & Positions","\u002Fapi\u002Fcomponents\u002Fcomponent-groups","4.api\u002F05.components\u002F4.component-groups",{"title":176,"path":177,"stem":178},"Page Data & Nested Pages","\u002Fapi\u002Fdynamic-pages","4.api\u002F06.dynamic-pages",{"title":180,"path":181,"stem":182},"Routes","\u002Fapi\u002Froutes","4.api\u002F07.routes",{"title":184,"path":185,"stem":186,"children":187,"page":128},"Data Fixtures","\u002Fapi\u002Fdata-fixtures","4.api\u002F08.data-fixtures",[188,192,196,200,204,208],{"title":189,"path":190,"stem":191},"Getting Started with Fixtures","\u002Fapi\u002Fdata-fixtures\u002Fgetting-started","4.api\u002F08.data-fixtures\u002F1.getting-started",{"title":193,"path":194,"stem":195},"Writing a Scaffold","\u002Fapi\u002Fdata-fixtures\u002Fwriting-a-scaffold","4.api\u002F08.data-fixtures\u002F2.writing-a-scaffold",{"title":197,"path":198,"stem":199},"Builder Reference","\u002Fapi\u002Fdata-fixtures\u002Fbuilder-reference","4.api\u002F08.data-fixtures\u002F3.builder-reference",{"title":201,"path":202,"stem":203},"Seeding Content","\u002Fapi\u002Fdata-fixtures\u002Fseeding-content","4.api\u002F08.data-fixtures\u002F4.seeding-content",{"title":205,"path":206,"stem":207},"Appending & Multiple Scaffolds","\u002Fapi\u002Fdata-fixtures\u002Fappending","4.api\u002F08.data-fixtures\u002F5.appending",{"title":209,"path":210,"stem":211},"Generating Fixtures","\u002Fapi\u002Fdata-fixtures\u002Fgenerating-fixtures","4.api\u002F08.data-fixtures\u002F6.generating-fixtures",{"title":213,"path":214,"stem":215},"Real-Time (Mercure)","\u002Fapi\u002Freal-time","4.api\u002F09.real-time",{"title":217,"path":218,"stem":219},"Versions & Conflicts","\u002Fapi\u002Fversions-and-conflicts","4.api\u002F09.versions-and-conflicts",{"title":221,"path":222,"stem":223},"HTTP Caching","\u002Fapi\u002Fhttp-caching","4.api\u002F10.http-caching",{"title":225,"path":226,"stem":227},"Orphaned Data","\u002Fapi\u002Forphaned-data","4.api\u002F11.orphaned-data",{"title":229,"path":230,"stem":231},"Console Commands","\u002Fapi\u002Fconsole-commands","4.api\u002F12.console-commands",{"title":233,"path":234,"stem":235},"Debugging & Errors","\u002Fapi\u002Fdebugging","4.api\u002F13.debugging",{"title":237,"path":238,"stem":239,"children":240,"collapsed":23},"Nuxt Module (Front-End)","\u002Fnuxt-module","5.nuxt-module\u002F0.index",[241,242,246,259,279,308,329,460,494,497,525,529],{"title":30,"path":238,"stem":239},{"title":243,"path":244,"stem":245},"Module Setup","\u002Fnuxt-module\u002Fmodule-setup","5.nuxt-module\u002F01.module-setup",{"title":247,"path":248,"stem":249,"children":250,"page":128},"Configuration","\u002Fnuxt-module\u002Fconfiguration","5.nuxt-module\u002F02.configuration",[251,255],{"title":252,"path":253,"stem":254},"Nuxt Config","\u002Fnuxt-module\u002Fconfiguration\u002Fnuxt-config","5.nuxt-module\u002F02.configuration\u002F1.nuxt-config",{"title":256,"path":257,"stem":258},"Site Config & SEO","\u002Fnuxt-module\u002Fconfiguration\u002Fsite-config-and-seo","5.nuxt-module\u002F02.configuration\u002F2.site-config-and-seo",{"title":260,"path":261,"stem":262,"children":263,"page":128},"Building Your Ui","\u002Fnuxt-module\u002Fbuilding-your-ui","5.nuxt-module\u002F03.building-your-ui",[264,268,272,275],{"title":265,"path":266,"stem":267},"Layouts","\u002Fnuxt-module\u002Fbuilding-your-ui\u002Fcreating-layouts","5.nuxt-module\u002F03.building-your-ui\u002F1.creating-layouts",{"title":269,"path":270,"stem":271},"Page Templates","\u002Fnuxt-module\u002Fbuilding-your-ui\u002Fcreating-page-templates","5.nuxt-module\u002F03.building-your-ui\u002F2.creating-page-templates",{"title":48,"path":273,"stem":274},"\u002Fnuxt-module\u002Fbuilding-your-ui\u002Fcreating-components","5.nuxt-module\u002F03.building-your-ui\u002F3.creating-components",{"title":276,"path":277,"stem":278},"CLI Generator","\u002Fnuxt-module\u002Fbuilding-your-ui\u002Fcwa-cli","5.nuxt-module\u002F03.building-your-ui\u002F4.cwa-cli",{"title":280,"path":281,"stem":282,"children":283,"page":128},"Component Helpers","\u002Fnuxt-module\u002Fcomponent-helpers","5.nuxt-module\u002F04.component-helpers",[284,288,292,296,300,304],{"title":285,"path":286,"stem":287},"Images & Media","\u002Fnuxt-module\u002Fcomponent-helpers\u002Fimages-and-uploads","5.nuxt-module\u002F04.component-helpers\u002F1.images-and-uploads",{"title":289,"path":290,"stem":291},"Collections & Pagination","\u002Fnuxt-module\u002Fcomponent-helpers\u002Fcollections-and-pagination","5.nuxt-module\u002F04.component-helpers\u002F2.collections-and-pagination",{"title":293,"path":294,"stem":295},"HTML Content","\u002Fnuxt-module\u002Fcomponent-helpers\u002Fhtml-content","5.nuxt-module\u002F04.component-helpers\u002F3.html-content",{"title":297,"path":298,"stem":299},"Rich-Text Editor","\u002Fnuxt-module\u002Fcomponent-helpers\u002Frich-text-editor","5.nuxt-module\u002F04.component-helpers\u002F4.rich-text-editor",{"title":301,"path":302,"stem":303},"Real-Time Updates","\u002Fnuxt-module\u002Fcomponent-helpers\u002Freal-time-updates","5.nuxt-module\u002F04.component-helpers\u002F5.real-time-updates",{"title":305,"path":306,"stem":307},"Forms","\u002Fnuxt-module\u002Fcomponent-helpers\u002Fforms","5.nuxt-module\u002F04.component-helpers\u002F6.forms",{"title":309,"path":310,"stem":311,"children":312,"page":128},"Cwa Components","\u002Fnuxt-module\u002Fcwa-components","5.nuxt-module\u002F05.cwa-components",[313,317,321,325],{"title":314,"path":315,"stem":316},"\u003CCwaComponentGroup \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-component-group","5.nuxt-module\u002F05.cwa-components\u002F1.cwa-component-group",{"title":318,"path":319,"stem":320},"\u003CCwaPage \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-page","5.nuxt-module\u002F05.cwa-components\u002F2.cwa-page",{"title":322,"path":323,"stem":324},"\u003CCwaLink \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-link","5.nuxt-module\u002F05.cwa-components\u002F3.cwa-link",{"title":326,"path":327,"stem":328},"\u003CCwaDefaultLayout \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-default-layout","5.nuxt-module\u002F05.cwa-components\u002F5.cwa-default-layout",{"title":330,"path":331,"stem":332,"children":333,"page":128},"Composables","\u002Fnuxt-module\u002Fcomposables","5.nuxt-module\u002F06.composables",[334,337,345,370,390,415,435],{"title":30,"path":335,"stem":336},"\u002Fnuxt-module\u002Fcomposables\u002Foverview","5.nuxt-module\u002F06.composables\u002F0.overview",{"title":338,"path":339,"stem":340,"children":341,"page":128},"Layout","\u002Fnuxt-module\u002Fcomposables\u002Flayout","5.nuxt-module\u002F06.composables\u002F1.layout",[342],{"title":338,"path":343,"stem":344},"\u002Fnuxt-module\u002Fcomposables\u002Flayout\u002Fuse-cwa-layout","5.nuxt-module\u002F06.composables\u002F1.layout\u002F1.use-cwa-layout",{"title":346,"path":347,"stem":348,"children":349,"page":128},"Component","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent","5.nuxt-module\u002F06.composables\u002F2.component",[350,354,358,362,366],{"title":351,"path":352,"stem":353},"Component (recommended)","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-component","5.nuxt-module\u002F06.composables\u002F2.component\u002F1.use-cwa-component",{"title":355,"path":356,"stem":357},"Resource","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-resource","5.nuxt-module\u002F06.composables\u002F2.component\u002F2.use-cwa-resource",{"title":359,"path":360,"stem":361},"File Field","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-file-field","5.nuxt-module\u002F06.composables\u002F2.component\u002F3.use-cwa-file-field",{"title":363,"path":364,"stem":365},"Collection Pagination","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-collection-pagination","5.nuxt-module\u002F06.composables\u002F2.component\u002F4.use-cwa-collection-pagination",{"title":367,"path":368,"stem":369},"Collection Resource (deprecated)","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-collection-resource","5.nuxt-module\u002F06.composables\u002F2.component\u002F9.use-cwa-collection-resource",{"title":371,"path":372,"stem":373,"children":374,"page":128},"Form","\u002Fnuxt-module\u002Fcomposables\u002Fform","5.nuxt-module\u002F06.composables\u002F3.form",[375,378,382,386],{"title":371,"path":376,"stem":377},"\u002Fnuxt-module\u002Fcomposables\u002Fform\u002Fuse-cwa-form","5.nuxt-module\u002F06.composables\u002F3.form\u002F1.use-cwa-form",{"title":379,"path":380,"stem":381},"Form Input","\u002Fnuxt-module\u002Fcomposables\u002Fform\u002Fuse-cwa-form-input","5.nuxt-module\u002F06.composables\u002F3.form\u002F2.use-cwa-form-input",{"title":383,"path":384,"stem":385},"Form Repeated","\u002Fnuxt-module\u002Fcomposables\u002Fform\u002Fuse-cwa-form-repeated","5.nuxt-module\u002F06.composables\u002F3.form\u002F3.use-cwa-form-repeated",{"title":387,"path":388,"stem":389},"Form Collection","\u002Fnuxt-module\u002Fcomposables\u002Fform\u002Fuse-cwa-form-collection","5.nuxt-module\u002F06.composables\u002F3.form\u002F4.use-cwa-form-collection",{"title":391,"path":392,"stem":393,"children":394,"page":128},"Admin Manager","\u002Fnuxt-module\u002Fcomposables\u002Fadmin-manager","5.nuxt-module\u002F06.composables\u002F4.admin-manager",[395,399,403,407,411],{"title":396,"path":397,"stem":398},"Manager Tab","\u002Fnuxt-module\u002Fcomposables\u002Fadmin-manager\u002Fuse-cwa-resource-manager-tab","5.nuxt-module\u002F06.composables\u002F4.admin-manager\u002F1.use-cwa-resource-manager-tab",{"title":400,"path":401,"stem":402},"Resource Model","\u002Fnuxt-module\u002Fcomposables\u002Fadmin-manager\u002Fuse-cwa-resource-model","5.nuxt-module\u002F06.composables\u002F4.admin-manager\u002F2.use-cwa-resource-model",{"title":404,"path":405,"stem":406},"Resource Upload","\u002Fnuxt-module\u002Fcomposables\u002Fadmin-manager\u002Fuse-cwa-resource-upload","5.nuxt-module\u002F06.composables\u002F4.admin-manager\u002F3.use-cwa-resource-upload",{"title":408,"path":409,"stem":410},"Remote Changes","\u002Fnuxt-module\u002Fcomposables\u002Fadmin-manager\u002Fon-remote-change","5.nuxt-module\u002F06.composables\u002F4.admin-manager\u002F4.on-remote-change",{"title":412,"path":413,"stem":414},"Real-Time Events","\u002Fnuxt-module\u002Fcomposables\u002Fadmin-manager\u002Fuse-cwa-realtime","5.nuxt-module\u002F06.composables\u002F4.admin-manager\u002F5.use-cwa-realtime",{"title":416,"path":417,"stem":418,"children":419,"page":128},"Utilities","\u002Fnuxt-module\u002Fcomposables\u002Futilities","5.nuxt-module\u002F06.composables\u002F5.utilities",[420,424,428,432],{"title":421,"path":422,"stem":423},"Resource Endpoint","\u002Fnuxt-module\u002Fcomposables\u002Futilities\u002Fuse-cwa-resource-endpoint","5.nuxt-module\u002F06.composables\u002F5.utilities\u002F1.use-cwa-resource-endpoint",{"title":425,"path":426,"stem":427},"Query Model","\u002Fnuxt-module\u002Fcomposables\u002Futilities\u002Fuse-query-bound-model","5.nuxt-module\u002F06.composables\u002F5.utilities\u002F2.use-query-bound-model",{"title":429,"path":430,"stem":431},"Resource Route","\u002Fnuxt-module\u002Fcomposables\u002Futilities\u002Fuse-cwa-resource-route","5.nuxt-module\u002F06.composables\u002F5.utilities\u002F3.use-cwa-resource-route",{"title":293,"path":433,"stem":434},"\u002Fnuxt-module\u002Fcomposables\u002Futilities\u002Fuse-html-content","5.nuxt-module\u002F06.composables\u002F5.utilities\u002F4.use-html-content",{"title":436,"path":437,"stem":438,"children":439,"page":128},"Auth","\u002Fnuxt-module\u002Fcomposables\u002Fauth","5.nuxt-module\u002F06.composables\u002F6.auth",[440,444,448,452,456],{"title":441,"path":442,"stem":443},"Login","\u002Fnuxt-module\u002Fcomposables\u002Fauth\u002Fuse-login","5.nuxt-module\u002F06.composables\u002F6.auth\u002F1.use-login",{"title":445,"path":446,"stem":447},"Forgot Password","\u002Fnuxt-module\u002Fcomposables\u002Fauth\u002Fuse-forgot-password","5.nuxt-module\u002F06.composables\u002F6.auth\u002F2.use-forgot-password",{"title":449,"path":450,"stem":451},"Reset Password","\u002Fnuxt-module\u002Fcomposables\u002Fauth\u002Fuse-reset-password","5.nuxt-module\u002F06.composables\u002F6.auth\u002F3.use-reset-password",{"title":453,"path":454,"stem":455},"Verify Email","\u002Fnuxt-module\u002Fcomposables\u002Fauth\u002Fuse-verify-email","5.nuxt-module\u002F06.composables\u002F6.auth\u002F4.use-verify-email",{"title":457,"path":458,"stem":459},"Resend Verify Email","\u002Fnuxt-module\u002Fcomposables\u002Fauth\u002Fuse-resend-verify-email","5.nuxt-module\u002F06.composables\u002F6.auth\u002F5.use-resend-verify-email",{"title":461,"path":462,"stem":463,"children":464,"page":128},"Cwa Api","\u002Fnuxt-module\u002Fcwa-api","5.nuxt-module\u002F07.cwa-api",[465,468,472,476,479,482,486,490],{"title":30,"path":466,"stem":467},"\u002Fnuxt-module\u002Fcwa-api\u002Foverview","5.nuxt-module\u002F07.cwa-api\u002F1.overview",{"title":469,"path":470,"stem":471},"Resources","\u002Fnuxt-module\u002Fcwa-api\u002Fresources","5.nuxt-module\u002F07.cwa-api\u002F2.resources",{"title":473,"path":474,"stem":475},"Resources Manager","\u002Fnuxt-module\u002Fcwa-api\u002Fresources-manager","5.nuxt-module\u002F07.cwa-api\u002F3.resources-manager",{"title":436,"path":477,"stem":478},"\u002Fnuxt-module\u002Fcwa-api\u002Fauth","5.nuxt-module\u002F07.cwa-api\u002F4.auth",{"title":305,"path":480,"stem":481},"\u002Fnuxt-module\u002Fcwa-api\u002Fforms","5.nuxt-module\u002F07.cwa-api\u002F5.forms",{"title":483,"path":484,"stem":485},"Admin","\u002Fnuxt-module\u002Fcwa-api\u002Fadmin","5.nuxt-module\u002F07.cwa-api\u002F6.admin",{"title":487,"path":488,"stem":489},"Site Config","\u002Fnuxt-module\u002Fcwa-api\u002Fsite-config","5.nuxt-module\u002F07.cwa-api\u002F7.site-config",{"title":491,"path":492,"stem":493},"Orphaned Resources","\u002Fnuxt-module\u002Fcwa-api\u002Forphaned-resources","5.nuxt-module\u002F07.cwa-api\u002F8.orphaned-resources",{"title":113,"path":495,"stem":496},"\u002Fnuxt-module\u002Fauthentication","5.nuxt-module\u002F08.authentication",{"title":498,"path":499,"stem":500,"children":501,"page":128},"Cwa Layer","\u002Fnuxt-module\u002Fcwa-layer","5.nuxt-module\u002F9.cwa-layer",[502,505,509,513,517,521],{"title":30,"path":503,"stem":504},"\u002Fnuxt-module\u002Fcwa-layer\u002Foverview","5.nuxt-module\u002F9.cwa-layer\u002F1.overview",{"title":506,"path":507,"stem":508},"Auth Pages","\u002Fnuxt-module\u002Fcwa-layer\u002Fauth-pages","5.nuxt-module\u002F9.cwa-layer\u002F2.auth-pages",{"title":510,"path":511,"stem":512},"Admin Panel","\u002Fnuxt-module\u002Fcwa-layer\u002Fadmin-panel","5.nuxt-module\u002F9.cwa-layer\u002F3.admin-panel",{"title":514,"path":515,"stem":516},"Styling","\u002Fnuxt-module\u002Fcwa-layer\u002Fstyling","5.nuxt-module\u002F9.cwa-layer\u002F4.styling",{"title":518,"path":519,"stem":520},"Date Pickers","\u002Fnuxt-module\u002Fcwa-layer\u002Fdate-pickers","5.nuxt-module\u002F9.cwa-layer\u002F5.date-pickers",{"title":522,"path":523,"stem":524},"Maintenance","\u002Fnuxt-module\u002Fcwa-layer\u002Fmaintenance","5.nuxt-module\u002F9.cwa-layer\u002F6.maintenance",{"title":526,"path":527,"stem":528},"PWA & Offline","\u002Fnuxt-module\u002Fpwa","5.nuxt-module\u002F91.pwa",{"title":530,"path":531,"stem":532},"Page Caching","\u002Fnuxt-module\u002Fpage-caching","5.nuxt-module\u002F92.page-caching",{"title":534,"path":535,"stem":536,"children":537,"collapsed":23},"Deployment & Operations","\u002Fdeployment","6.deployment\u002F0.index",[538,540,544,548,552,556,560,564,568,572,576,580,584,588,592,596,600],{"title":539,"path":535,"stem":536},"Deploying a CWA Site",{"title":541,"path":542,"stem":543},"Docker","\u002Fdeployment\u002Fdocker","6.deployment\u002F1.docker",{"title":545,"path":546,"stem":547},"Deploy to One Server","\u002Fdeployment\u002Fone-server","6.deployment\u002F1.one-server",{"title":549,"path":550,"stem":551},"Kubernetes & Helm","\u002Fdeployment\u002Fkubernetes","6.deployment\u002F2.kubernetes",{"title":553,"path":554,"stem":555},"Moving From One Server to Kubernetes","\u002Fdeployment\u002Fmoving-to-kubernetes","6.deployment\u002F2.moving-to-kubernetes",{"title":557,"path":558,"stem":559},"Scaled Mode","\u002Fdeployment\u002Fscaled-mode","6.deployment\u002F2.scaled-mode",{"title":561,"path":562,"stem":563},"CI\u002FCD","\u002Fdeployment\u002Fci-cd","6.deployment\u002F3.ci-cd",{"title":565,"path":566,"stem":567},"Migrations and Backups","\u002Fdeployment\u002Fmigrations-and-backups","6.deployment\u002F3.migrations-and-backups",{"title":569,"path":570,"stem":571},"CI\u002FCD Variables","\u002Fdeployment\u002Fvariables","6.deployment\u002F4.variables",{"title":573,"path":574,"stem":575},"Cloudflare","\u002Fdeployment\u002Fcloudflare","6.deployment\u002F5.cloudflare",{"title":577,"path":578,"stem":579},"Origin Protection","\u002Fdeployment\u002Forigin-protection","6.deployment\u002F5.origin-protection",{"title":581,"path":582,"stem":583},"Load Testing","\u002Fdeployment\u002Fload-testing","6.deployment\u002F6.load-testing",{"title":585,"path":586,"stem":587},"Performance Audit","\u002Fdeployment\u002Fperformance-audit","6.deployment\u002F6.performance-audit",{"title":589,"path":590,"stem":591},"Reading Lighthouse Scores","\u002Fdeployment\u002Freading-lighthouse-scores","6.deployment\u002F6.reading-lighthouse-scores",{"title":593,"path":594,"stem":595},"Browser Tests","\u002Fdeployment\u002Fbrowser-tests","6.deployment\u002F7.browser-tests",{"title":597,"path":598,"stem":599},"Faster Functional Tests","\u002Fdeployment\u002Ffaster-functional-tests","6.deployment\u002F7.faster-functional-tests",{"title":601,"path":602,"stem":603},"Troubleshooting","\u002Fdeployment\u002Ftroubleshooting","6.deployment\u002F7.troubleshooting",{"id":605,"title":593,"badge":606,"body":607,"description":1924,"extension":1925,"links":606,"meta":1926,"navigation":23,"path":594,"seo":1927,"stem":595,"__hash__":1928},"docs\u002F6.deployment\u002F7.browser-tests.md",null,{"type":608,"value":609,"toc":1913},"minimark",[610,619,626,631,641,644,666,673,676,679,727,730,745,749,769,822,825,828,849,893,911,918,957,974,998,1005,1021,1053,1056,1059,1070,1074,1093,1111,1133,1137,1140,1226,1350,1360,1372,1376,1387,1672,1739,1743,1749,1783,1792,1842,1847,1894,1909],[611,612,613,614,618],"p",{},"The template has opt-in browser tests, in ",[615,616,617],"code",{},"e2e\u002F",". They take the php and app images this branch just built, start them in a throwaway stack, and use the site in Chromium the way visitors and editors do. When they're on, every deploy waits for them, so a build that breaks rendering a page or signing in never reaches a site.",[611,620,621,622,625],{},"The template's ",[615,623,624],{},"e2e\u002FREADME.md"," is the short version of this page.",[627,628,630],"h2",{"id":629},"what-they-check","What they check",[611,632,633,634,636,637,640],{},"The template's tests assume only the admin user and the pages its sitemap lists. They don't use the template's sample content, so a project can keep ",[615,635,617],{}," unchanged and the tests pass on its own site. The media checks run only when the project has the template's ",[615,638,639],{},"Image"," component.",[611,642,643],{},"They cover:",[645,646,647,651,654,657,660,663],"ul",{},[648,649,650],"li",{},"every page in the sitemap renders and hydrates;",[648,652,653],{},"the first page in the sitemap is served from the cache on its second request;",[648,655,656],{},"the loading bar shows while a page loads after a client-side navigation, from the first sitemap page to the second;",[648,658,659],{},"the first sitemap page's OG image renders;",[648,661,662],{},"an admin can log in and out;",[648,664,665],{},"a password reset email links to a working reset page.",[611,667,668,669,672],{},"So the sitemap needs at least two pages, and its first page needs an ",[615,670,671],{},"og:image"," tag, or those tests fail.",[611,674,675],{},"Editing and publishing content isn't tested here: the module and the bundle test that.",[611,677,678],{},"A run also fails when:",[645,680,681,696,699,708,720],{},[648,682,683,684,687,688,691,692,695],{},"an API write gets a ",[615,685,686],{},"405",", ",[615,689,690],{},"406"," or ",[615,693,694],{},"415",", which means the front end and the API disagree about a method or a content type;",[648,697,698],{},"the page throws an uncaught error;",[648,700,701,702,707],{},"a cache or media check fails (see ",[703,704,706],"a",{"href":705},"#the-test-stack","The test stack",");",[648,709,710,711,715,716,719],{},"the API sent an error mail (",[703,712,714],{"href":713},"\u002Fdeployment\u002Fkubernetes#error-mail","Kubernetes & Helm › Error mail","), meaning it answered a request with a ",[615,717,718],{},"5xx"," that the UI may have hidden;",[648,721,722,723,726],{},"not every listed test ran and passed (",[615,724,725],{},"check-all-ran.mjs","), so a skipped or filtered-out test can't pass a run.",[611,728,729],{},"Every timeout is bounded: 90 seconds a test, 30 seconds a navigation, 15 seconds an action and 10 seconds an assertion.",[731,732,734],"callout",{"icon":733},"i-heroicons-photo",[611,735,736,740,741,744],{},[737,738,739],"strong",{},"Image placeholder:"," the Playwright HTML report from a passing ",[615,742,743],{},"browser tests"," job, with the list of tests.",[627,746,748],{"id":747},"turning-them-on","Turning them on",[611,750,751,752,755,756,759,760,764,765,768],{},"Set the CI variable ",[615,753,754],{},"CWA_CI_E2E_ENABLED"," to ",[615,757,758],{},"\"true\""," (see ",[703,761,763],{"href":762},"\u002Fdeployment\u002Fvariables#pipeline-flags","CI\u002FCD Variables › Pipeline flags","). Nothing runs until you do. ",[615,766,767],{},"CWA_CI_TEST_DISABLED=\"true\""," skips them along with the other tests.",[645,770,771,800],{},[648,772,773,776,777,779,780,783,784,787,788,791,792,795,796,799],{},[737,774,775],{},"GitLab:"," a ",[615,778,743],{}," job in the ",[615,781,782],{},"test"," stage, after ",[615,785,786],{},"build api"," and ",[615,789,790],{},"build app",". The review, staging, canary and production jobs ",[615,793,794],{},"needs:"," it, so none of them runs when it fails. It runs Docker in Docker (",[615,797,798],{},"docker:27-dind",").",[648,801,802,805,806,809,810,813,814,817,818,821],{},[737,803,804],{},"GitHub:"," the reusable workflow ",[615,807,808],{},".github\u002Fworkflows\u002Fbrowser-tests.yml",". ",[615,811,812],{},"ci.yml"," calls it after the builds, before review and staging, and ",[615,815,816],{},"production.yml"," calls it on ",[615,819,820],{},"main","'s images before the deploy. A failed run stops those deploys. It has a 30-minute limit.",[611,823,824],{},"The Playwright report, with traces, screenshots and videos of failed tests, is kept as a job artifact for 14 days. GitLab also reads its JUnit file, so the results show in the pipeline's test report.",[627,826,706],{"id":827},"the-test-stack",[611,829,830,833,834,787,837,759,840,844,845,848],{},[615,831,832],{},"e2e\u002Frun.sh"," boots the single-server production configuration: the project's ",[615,835,836],{},"compose.yaml",[615,838,839],{},"compose.prod.yaml",[703,841,843],{"href":842},"\u002Fdeployment\u002Fdocker#production-docker-compose","Docker › Production Docker Compose","), with ",[615,846,847],{},"e2e\u002Fcompose.yaml"," on top adding only what a test run needs:",[645,850,851,865,872,883,886],{},[648,852,853,854,857,858,787,861,864],{},"the branch's php and app ",[737,855,856],{},"production"," images (",[615,859,860],{},"frankenphp_prod",[615,862,863],{},"prod",") in place of builds, so a run tests the images CI built and never builds its own;",[648,866,867,868,871],{},"PostgreSQL on ",[615,869,870],{},"tmpfs",", so every run starts empty;",[648,873,874,875,878,879,707],{},"a Cloud Storage emulator, reached through php's ",[615,876,877],{},"GCLOUD_API_ENDPOINT",", used only when you ask for it (see ",[703,880,882],{"href":881},"#media-storage-in-the-tests","Media storage in the tests",[648,884,885],{},"Mailpit, which catches every email, such as the password reset;",[648,887,888,889,892],{},"the Playwright runner, built from ",[615,890,891],{},"e2e\u002FDockerfile",".",[611,894,895,896,898,899,902,903,906,907,910],{},"Every production setting comes from ",[615,897,839],{},", so a change there that breaks the site fails the tests too. ",[615,900,901],{},"run.sh"," sets the values it requires, with ",[615,904,905],{},"SITE_HOST=localhost",", a new set of secrets and a new JWT key pair each run, and unsets every other variable the compose files read, so nothing from your shell, the CI job or a local ",[615,908,909],{},".env"," reaches the stack.",[611,912,913,914,917],{},"It waits for the stack to be healthy (migrations run on boot), loads the fixtures, flushes the HTTP cache and waits for the site. Then, before any browser test, it checks the cache on the production configuration. ",[615,915,916],{},"e2e\u002Fcache-checks.sh"," runs in the php container and prints one line for each check:",[645,919,920,927,941,951],{},[648,921,922,923,926],{},"the API answers over HTTPS, and over the plain-HTTP ",[615,924,925],{},"http:\u002F\u002Fphp.local"," route the Nuxt server renders with, without a redirect;",[648,928,929,930,687,933,936,937,940],{},"every page in the sitemap is a ",[615,931,932],{},"200",[615,934,935],{},"stored"," on its first view and a ",[615,938,939],{},"hit"," on its second;",[648,942,943,944,947,948,950],{},"when the sitemap lists two or more pages, purging one cache tag (",[615,945,946],{},"Surrogate-Key",") of the first page that the second doesn't have, a component's own tag when there is one, renders the first page again, while the second page stays a ",[615,949,939],{},". Each page has its own route tag, so there is always one. A one-page sitemap skips this check;",[648,952,953,956],{},[615,954,955],{},"purge-rendered-html"," renders every page again, while the API stays cached.",[611,958,959,960,962,963,965,966,969,970,973],{},"Then ",[615,961,901],{}," checks that Caddy's admin API (port 2019) can't be reached from the app container, and, when the project has the ",[615,964,639],{}," component (",[615,967,968],{},"api\u002Fsrc\u002FEntity\u002FImage.php","), runs the media checks in ",[615,971,972],{},"e2e\u002Fmedia-checks.sh",":",[645,975,976,982,993],{},[648,977,978,979,981],{},"the admin signs in and uploads a PNG to the ",[615,980,639],{}," component;",[648,983,984,985,988,989,992],{},"the original is served as ",[615,986,987],{},"image\u002Fpng",", and each image variant as ",[615,990,991],{},"image\u002Fwebp",";",[648,994,995,997],{},[615,996,901],{}," recreates the php container, and every one of those files is still served.",[611,999,1000,1001,1004],{},"Then it flushes the cache again so the browser tests start cold. A failed check fails the run, with ",[615,1002,1003],{},"FAILED:"," and what it wanted on the check's line.",[611,1006,1007,1008,1010,1011,1013,1014,1016,1017,1020],{},"So every page in the sitemap must be cacheable for a visitor: a page that is never ",[615,1009,935],{}," fails the run. The checks need at least one page in the sitemap. Without the ",[615,1012,639],{}," component, ",[615,1015,901],{}," prints ",[615,1018,1019],{},"skipped: media checks (no Image component in this project)"," and goes on.",[611,1022,1023,1025,1026,1028,1029,1031,1032,687,1034,687,1037,687,1040,687,1043,687,1046,787,1049,1052],{},[615,1024,901],{}," relies on the project's own ",[615,1027,839],{},", so copy ",[615,1030,617],{}," together with ",[615,1033,839],{},[615,1035,1036],{},"api\u002Ffrankenphp\u002Fdocker-entrypoint.sh",[615,1038,1039],{},"api\u002Ffrankenphp\u002Fsite-hosts.sh",[615,1041,1042],{},"api\u002Ffrankenphp\u002FCaddyfile",[615,1044,1045],{},"api\u002Fconfig\u002Fservices.php",[615,1047,1048],{},"api\u002Fsrc\u002FFlysystem\u002F",[615,1050,1051],{},"api\u002FDockerfile"," when you take the tests into a project.",[611,1054,1055],{},"After the tests, it stops and deletes the stack.",[611,1057,1058],{},"The stack publishes no ports, so it can run beside your dev stack.",[611,1060,1061,1062,1065,1066,1069],{},"Each checkout gets its own stack, named ",[615,1063,1064],{},"cwa-e2e-\u003Cdirectory>"," after the checkout's directory, and its own tests container. So two projects can run their tests on one machine at once, and one run's clean-up never removes another's containers or volumes. ",[615,1067,1068],{},"COMPOSE_PROJECT_NAME"," sets a different name.",[1071,1072,882],"h3",{"id":1073},"media-storage-in-the-tests",[611,1075,1076,1077,1079,1080,1083,1084,1087,1088,1092],{},"By default the stack stores media on the server, as ",[615,1078,839],{}," does (",[615,1081,1082],{},"MEDIA_STORAGE=local",", in its ",[615,1085,1086],{},"media_data"," volume; see ",[703,1089,1091],{"href":1090},"\u002Fdeployment\u002Fdocker#media-on-one-server","Docker › Media on one server","). That's why recreating php must keep the uploaded files.",[611,1094,1095,1096,1099,1100,1103,1104,1106,1107,1110],{},"To run the same tests with media in a bucket, as Kubernetes deploys store it, set ",[615,1097,1098],{},"CWA_CI_E2E_STORAGE=\"gcloud\""," in CI, or ",[615,1101,1102],{},"E2E_STORAGE=gcloud"," locally. php then stores media in the Cloud Storage emulator. ",[615,1105,877],{}," is the only change this needs in the API: ",[615,1108,1109],{},"GoogleCloudStorageFactory"," uses an emulator, with no credentials, when it's set. Leave it unset on every deploy, where empty means Google.",[611,1112,1113,1114,687,1118,1121,1122,1124,1125,1128,1129,1132],{},"The stack also sets a review app's ",[703,1115,1117],{"href":1116},"\u002Fdeployment\u002Fvariables#storage-path-prefix","storage path prefix",[615,1119,1120],{},"GCLOUD_PATH_PREFIX: review\u002Fe2e\u002F",". After the tests, ",[615,1123,901],{}," checks that every stored file is under it, in the bucket or in ",[615,1126,1127],{},"public\u002Fuploads",", then runs the same guarded ",[615,1130,1131],{},"delete-stored-files"," a stopped review app runs and checks nothing is left. Either failure fails the run. The check covers the media checks' upload and whatever your fixtures and your own tests store.",[627,1134,1136],{"id":1135},"running-them-locally","Running them locally",[611,1138,1139],{},"You need Docker and openssl. Build the two production images, then run the tests against them:",[1141,1142,1147],"pre",{"className":1143,"code":1144,"language":1145,"meta":1146,"style":1146},"language-bash shiki shiki-themes github-light github-dark material-theme-palenight","docker build --target frankenphp_prod -t cwa-e2e-php:local api\ndocker build --target prod -t cwa-e2e-app:local app\nE2E_PHP_IMAGE=cwa-e2e-php:local E2E_APP_IMAGE=cwa-e2e-app:local sh e2e\u002Frun.sh\n","bash","",[615,1148,1149,1178,1198],{"__ignoreMap":1146},[1150,1151,1154,1158,1162,1166,1169,1172,1175],"span",{"class":1152,"line":1153},"line",1,[1150,1155,1157],{"class":1156},"sRCss","docker",[1150,1159,1161],{"class":1160},"sLL54"," build",[1150,1163,1165],{"class":1164},"szhYu"," --target",[1150,1167,1168],{"class":1160}," frankenphp_prod",[1150,1170,1171],{"class":1164}," -t",[1150,1173,1174],{"class":1160}," cwa-e2e-php:local",[1150,1176,1177],{"class":1160}," api\n",[1150,1179,1181,1183,1185,1187,1190,1192,1195],{"class":1152,"line":1180},2,[1150,1182,1157],{"class":1156},[1150,1184,1161],{"class":1160},[1150,1186,1165],{"class":1164},[1150,1188,1189],{"class":1160}," prod",[1150,1191,1171],{"class":1164},[1150,1193,1194],{"class":1160}," cwa-e2e-app:local",[1150,1196,1197],{"class":1160}," app\n",[1150,1199,1201,1205,1209,1212,1215,1217,1220,1223],{"class":1152,"line":1200},3,[1150,1202,1204],{"class":1203},"sPB8G","E2E_PHP_IMAGE",[1150,1206,1208],{"class":1207},"sVlFx","=",[1150,1210,1211],{"class":1160},"cwa-e2e-php:local",[1150,1213,1214],{"class":1203}," E2E_APP_IMAGE",[1150,1216,1208],{"class":1207},[1150,1218,1219],{"class":1160},"cwa-e2e-app:local",[1150,1221,1222],{"class":1156}," sh",[1150,1224,1225],{"class":1160}," e2e\u002Frun.sh\n",[1227,1228,1229,1242],"table",{},[1230,1231,1232],"thead",{},[1233,1234,1235,1239],"tr",{},[1236,1237,1238],"th",{},"Variable",[1236,1240,1241],{},"What it does",[1243,1244,1245,1262,1280,1296,1308,1330],"tbody",{},[1233,1246,1247,1256],{},[1248,1249,1250,1252,1253],"td",{},[615,1251,1204],{}," \u002F ",[615,1254,1255],{},"E2E_APP_IMAGE",[1248,1257,1258,1261],{},[737,1259,1260],{},"Required."," The php and app images to test. CI passes this branch's tags",[1233,1263,1264,1269],{},[1248,1265,1266],{},[615,1267,1268],{},"E2E_ARGS",[1248,1270,1271,1272,1275,1276,1279],{},"Runs part of the suite: a file, such as ",[615,1273,1274],{},"E2E_ARGS=\"tests\u002Fadmin.spec.ts\"",", or ",[615,1277,1278],{},"--grep …",". The every-test-ran check is skipped then",[1233,1281,1282,1287],{},[1248,1283,1284],{},[615,1285,1286],{},"E2E_KEEP_STACK",[1248,1288,1289,1292,1293,892],{},[615,1290,1291],{},"true"," leaves the stack running afterwards, and prints the admin password and the command that stops it, from the repository root: ",[615,1294,1295],{},"docker compose -p \u003Cstack> down -v",[1233,1297,1298,1302],{},[1248,1299,1300],{},[615,1301,1068],{},[1248,1303,1304,1305,1307],{},"The stack's name. Defaults to ",[615,1306,1064],{},", after the checkout's directory",[1233,1309,1310,1315],{},[1248,1311,1312],{},[615,1313,1314],{},"E2E_SCALED",[1248,1316,1317,1319,1320,1323,1324,1327,1328,892],{},[615,1318,1291],{}," runs the tests against scaled mode: two php replicas sharing one Redis cache and one Mercure hub, from ",[615,1321,1322],{},"e2e\u002Fcompose.scaled.yaml",". In CI, set ",[615,1325,1326],{},"CWA_CI_E2E_SCALED=\"true\"",". See ",[703,1329,557],{"href":558},[1233,1331,1332,1337],{},[1248,1333,1334],{},[615,1335,1336],{},"E2E_STORAGE",[1248,1338,1339,1342,1343,1323,1346,1327,1348,892],{},[615,1340,1341],{},"gcloud"," stores media in the Cloud Storage emulator instead of on the server. Defaults to ",[615,1344,1345],{},"local",[615,1347,1098],{},[703,1349,882],{"href":881},[611,1351,1352,1353,787,1356,1359],{},"The reports land in ",[615,1354,1355],{},"e2e\u002Fplaywright-report\u002F",[615,1357,1358],{},"e2e\u002Ftest-results\u002F",". When a run fails, the script prints the last 80 lines of the php and app logs.",[731,1361,1363],{"icon":1362},"i-heroicons-information-circle",[611,1364,1365,1371],{},[737,1366,1367,1368],{},"On an ARM Mac, set ",[615,1369,1370],{},"DOCKER_DEFAULT_PLATFORM=linux\u002Famd64"," to test images from your CI registry, which builds them for amd64 only.",[627,1373,1375],{"id":1374},"writing-your-own-tests","Writing your own tests",[611,1377,1378,1379,1382,1383,1386],{},"The template's tests check only what every CWA site has. Your project's own specs are where its pages and content get tested: add them under ",[615,1380,1381],{},"e2e\u002Ftests\u002F",", as ",[615,1384,1385],{},"*.spec.ts"," files.",[1141,1388,1393],{"className":1389,"code":1390,"filename":1391,"language":1392,"meta":1146,"style":1146},"language-ts shiki shiki-themes github-light github-dark material-theme-palenight","import { expect, logIn, test } from '.\u002Ffixtures'\n\ntest('an admin sees the contact page, and so does a visitor', async ({ page, visitor }) => {\n  await logIn(page)\n  await page.goto('\u002Fcontact')\n  await expect(page.getByRole('button', { name: 'Edit' })).toBeVisible()\n\n  await visitor.goto('\u002Fcontact')\n  await expect(visitor.getByRole('heading', { name: 'Contact us', exact: true })).toBeVisible()\n})\n","e2e\u002Ftests\u002Fcontact.spec.ts","ts",[615,1394,1395,1435,1440,1483,1500,1523,1577,1582,1603,1664],{"__ignoreMap":1146},[1150,1396,1397,1401,1405,1408,1411,1414,1416,1419,1422,1425,1429,1432],{"class":1152,"line":1153},[1150,1398,1400],{"class":1399},"sm4w6","import",[1150,1402,1404],{"class":1403},"sOvfz"," {",[1150,1406,1407],{"class":1203}," expect",[1150,1409,1410],{"class":1403},",",[1150,1412,1413],{"class":1203}," logIn",[1150,1415,1410],{"class":1403},[1150,1417,1418],{"class":1203}," test",[1150,1420,1421],{"class":1403}," }",[1150,1423,1424],{"class":1399}," from",[1150,1426,1428],{"class":1427},"seSrl"," '",[1150,1430,1431],{"class":1160},".\u002Ffixtures",[1150,1433,1434],{"class":1427},"'\n",[1150,1436,1437],{"class":1152,"line":1180},[1150,1438,1439],{"emptyLinePlaceholder":23},"\n",[1150,1441,1442,1445,1448,1451,1454,1456,1458,1462,1465,1469,1471,1474,1477,1480],{"class":1152,"line":1200},[1150,1443,782],{"class":1444},"sKpYG",[1150,1446,1447],{"class":1203},"(",[1150,1449,1450],{"class":1427},"'",[1150,1452,1453],{"class":1160},"an admin sees the contact page, and so does a visitor",[1150,1455,1450],{"class":1427},[1150,1457,1410],{"class":1403},[1150,1459,1461],{"class":1460},"swB56"," async",[1150,1463,1464],{"class":1403}," ({",[1150,1466,1468],{"class":1467},"sNTR6"," page",[1150,1470,1410],{"class":1403},[1150,1472,1473],{"class":1467}," visitor",[1150,1475,1476],{"class":1403}," })",[1150,1478,1479],{"class":1460}," =>",[1150,1481,1482],{"class":1403}," {\n",[1150,1484,1486,1489,1491,1494,1497],{"class":1152,"line":1485},4,[1150,1487,1488],{"class":1399},"  await",[1150,1490,1413],{"class":1444},[1150,1492,1447],{"class":1493},"sDHlG",[1150,1495,1496],{"class":1203},"page",[1150,1498,1499],{"class":1493},")\n",[1150,1501,1503,1505,1507,1509,1512,1514,1516,1519,1521],{"class":1152,"line":1502},5,[1150,1504,1488],{"class":1399},[1150,1506,1468],{"class":1203},[1150,1508,892],{"class":1403},[1150,1510,1511],{"class":1444},"goto",[1150,1513,1447],{"class":1493},[1150,1515,1450],{"class":1427},[1150,1517,1518],{"class":1160},"\u002Fcontact",[1150,1520,1450],{"class":1427},[1150,1522,1499],{"class":1493},[1150,1524,1526,1528,1530,1532,1534,1536,1539,1541,1543,1546,1548,1550,1552,1555,1557,1559,1562,1564,1566,1569,1571,1574],{"class":1152,"line":1525},6,[1150,1527,1488],{"class":1399},[1150,1529,1407],{"class":1444},[1150,1531,1447],{"class":1493},[1150,1533,1496],{"class":1203},[1150,1535,892],{"class":1403},[1150,1537,1538],{"class":1444},"getByRole",[1150,1540,1447],{"class":1493},[1150,1542,1450],{"class":1427},[1150,1544,1545],{"class":1160},"button",[1150,1547,1450],{"class":1427},[1150,1549,1410],{"class":1403},[1150,1551,1404],{"class":1403},[1150,1553,1554],{"class":1493}," name",[1150,1556,973],{"class":1403},[1150,1558,1428],{"class":1427},[1150,1560,1561],{"class":1160},"Edit",[1150,1563,1450],{"class":1427},[1150,1565,1421],{"class":1403},[1150,1567,1568],{"class":1493},"))",[1150,1570,892],{"class":1403},[1150,1572,1573],{"class":1444},"toBeVisible",[1150,1575,1576],{"class":1493},"()\n",[1150,1578,1580],{"class":1152,"line":1579},7,[1150,1581,1439],{"emptyLinePlaceholder":23},[1150,1583,1585,1587,1589,1591,1593,1595,1597,1599,1601],{"class":1152,"line":1584},8,[1150,1586,1488],{"class":1399},[1150,1588,1473],{"class":1203},[1150,1590,892],{"class":1403},[1150,1592,1511],{"class":1444},[1150,1594,1447],{"class":1493},[1150,1596,1450],{"class":1427},[1150,1598,1518],{"class":1160},[1150,1600,1450],{"class":1427},[1150,1602,1499],{"class":1493},[1150,1604,1606,1608,1610,1612,1615,1617,1619,1621,1623,1626,1628,1630,1632,1634,1636,1638,1641,1643,1645,1648,1650,1654,1656,1658,1660,1662],{"class":1152,"line":1605},9,[1150,1607,1488],{"class":1399},[1150,1609,1407],{"class":1444},[1150,1611,1447],{"class":1493},[1150,1613,1614],{"class":1203},"visitor",[1150,1616,892],{"class":1403},[1150,1618,1538],{"class":1444},[1150,1620,1447],{"class":1493},[1150,1622,1450],{"class":1427},[1150,1624,1625],{"class":1160},"heading",[1150,1627,1450],{"class":1427},[1150,1629,1410],{"class":1403},[1150,1631,1404],{"class":1403},[1150,1633,1554],{"class":1493},[1150,1635,973],{"class":1403},[1150,1637,1428],{"class":1427},[1150,1639,1640],{"class":1160},"Contact us",[1150,1642,1450],{"class":1427},[1150,1644,1410],{"class":1403},[1150,1646,1647],{"class":1493}," exact",[1150,1649,973],{"class":1403},[1150,1651,1653],{"class":1652},"swWMF"," true",[1150,1655,1421],{"class":1403},[1150,1657,1568],{"class":1493},[1150,1659,892],{"class":1403},[1150,1661,1573],{"class":1444},[1150,1663,1576],{"class":1493},[1150,1665,1667,1670],{"class":1152,"line":1666},10,[1150,1668,1669],{"class":1403},"}",[1150,1671,1499],{"class":1203},[645,1673,1674,1699,1714,1729],{},[648,1675,1676,1688,1689,1692,1693,1695,1696,1698],{},[737,1677,1678,1679,787,1681,1684,1685],{},"Import ",[615,1680,782],{},[615,1682,1683],{},"expect"," from ",[615,1686,1687],{},"tests\u002Ffixtures.ts",", never from ",[615,1690,1691],{},"@playwright\u002Ftest",". Its ",[615,1694,1496],{}," fixture is what fails a test on a refused API write or an uncaught error. Importing types from ",[615,1697,1691],{}," is fine.",[648,1700,1701,1707,1708,1711,1712,892],{},[737,1702,1703,1706],{},[615,1704,1705],{},"logIn(page)"," signs in as the admin."," The password is made for each run and loaded with the fixtures. It waits for ",[615,1709,1710],{},"\u002Flogin"," to hydrate before it fills the form, because before that the form submits natively and reloads ",[615,1713,1710],{},[648,1715,1716,1722,1723,1725,1726,1728],{},[737,1717,1718,1719,1721],{},"Use the ",[615,1720,1614],{}," fixture to see a page as an anonymous visitor",", beside the signed-in ",[615,1724,1496],{},". It has its own browser context, is checked the same way as ",[615,1727,1496],{},", and is closed after the test.",[648,1730,1731,1734,1735,1738],{},[737,1732,1733],{},"Tests share one database and run one at a time."," A test that edits content should use its own unique text, such as one with ",[615,1736,1737],{},"Date.now()"," in it, so it can't match another test's.",[1071,1740,1742],{"id":1741},"type-checks-lint-and-conventions","Type checks, lint and conventions",[611,1744,1745,1746,1748],{},"The tests are type-checked and linted before they run, and every rule is an error, so a type or lint error fails the ",[615,1747,743],{}," job. Playwright only strips types, so nothing else would catch them.",[645,1750,1751,1760],{},[648,1752,1753,1756,1757,992],{},[615,1754,1755],{},"tsc",", with the strict ",[615,1758,1759],{},"e2e\u002Ftsconfig.json",[648,1761,1762,1763,1766,1767,1770,1771,1774,1775,1778,1779,1782],{},"ESLint, with ",[615,1764,1765],{},"e2e\u002Feslint.config.mjs",": ",[615,1768,1769],{},"@nuxt\u002Feslint-config","'s stylistic rules (the module's style), ",[615,1772,1773],{},"eslint-plugin-playwright",", and typescript-eslint's ",[615,1776,1777],{},"no-floating-promises",", which also catches a missing ",[615,1780,1781],{},"await"," on your own helpers.",[611,1784,1785,1786,809,1788,1791],{},"Run them yourself in ",[615,1787,617],{},[615,1789,1790],{},"npm ci"," also gives your editor the types:",[1141,1793,1795],{"className":1143,"code":1794,"language":1145,"meta":1146,"style":1146},"cd e2e\nnpm ci\nnpm run typecheck\nnpm run lint\nnpm run lint:fix\n",[615,1796,1797,1806,1814,1824,1833],{"__ignoreMap":1146},[1150,1798,1799,1803],{"class":1152,"line":1153},[1150,1800,1802],{"class":1801},"s3Ny6","cd",[1150,1804,1805],{"class":1160}," e2e\n",[1150,1807,1808,1811],{"class":1152,"line":1180},[1150,1809,1810],{"class":1156},"npm",[1150,1812,1813],{"class":1160}," ci\n",[1150,1815,1816,1818,1821],{"class":1152,"line":1200},[1150,1817,1810],{"class":1156},[1150,1819,1820],{"class":1160}," run",[1150,1822,1823],{"class":1160}," typecheck\n",[1150,1825,1826,1828,1830],{"class":1152,"line":1485},[1150,1827,1810],{"class":1156},[1150,1829,1820],{"class":1160},[1150,1831,1832],{"class":1160}," lint\n",[1150,1834,1835,1837,1839],{"class":1152,"line":1502},[1150,1836,1810],{"class":1156},[1150,1838,1820],{"class":1160},[1150,1840,1841],{"class":1160}," lint:fix\n",[611,1843,1844,1845,973],{},"The rules come with these conventions, from the template's ",[615,1846,624],{},[645,1848,1849,1855,1867,1880],{},[648,1850,1851,1854],{},[737,1852,1853],{},"Await every Playwright call and every helper"," that returns a promise.",[648,1856,1857,1766,1860,1863,1864,892],{},[737,1858,1859],{},"Use retrying assertions",[615,1861,1862],{},"await expect(locator).toBeVisible()",", not ",[615,1865,1866],{},"expect(await locator.isVisible())",[648,1868,1869,1872,1873,787,1876,1879],{},[737,1870,1871],{},"No fixed waits and no forced clicks."," ",[615,1874,1875],{},"waitForTimeout",[615,1877,1878],{},"force: true"," are errors. Wait for the response, URL or element the test is about.",[648,1881,1882,1766,1885,687,1887,691,1890,1893],{},[737,1883,1884],{},"Find elements the way a person does",[615,1886,1538],{},[615,1888,1889],{},"getByLabel",[615,1891,1892],{},"getByText",", with exact names. Use a CSS selector only where the markup gives nothing better, with a comment that says why.",[731,1895,1897],{"icon":1896},"i-heroicons-light-bulb",[611,1898,1899,1900,787,1902,1904,1905,1908],{},"Keep the Playwright image in ",[615,1901,891],{},[615,1903,1691],{}," in ",[615,1906,1907],{},"e2e\u002Fpackage.json"," on the same version.",[1910,1911,1912],"style",{},"html pre.shiki code .sRCss, html code.shiki .sRCss{--shiki-light:#6F42C1;--shiki-default:#B392F0;--shiki-dark:#FFCB6B}html pre.shiki code .sLL54, html code.shiki .sLL54{--shiki-light:#032F62;--shiki-default:#9ECBFF;--shiki-dark:#C3E88D}html pre.shiki code .szhYu, html code.shiki .szhYu{--shiki-light:#005CC5;--shiki-default:#79B8FF;--shiki-dark:#C3E88D}html pre.shiki code .sPB8G, html code.shiki .sPB8G{--shiki-light:#24292E;--shiki-default:#E1E4E8;--shiki-dark:#BABED8}html pre.shiki code .sVlFx, html code.shiki .sVlFx{--shiki-light:#D73A49;--shiki-default:#F97583;--shiki-dark:#89DDFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .sm4w6, html code.shiki .sm4w6{--shiki-light:#D73A49;--shiki-light-font-style:inherit;--shiki-default:#F97583;--shiki-default-font-style:inherit;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sOvfz, html code.shiki .sOvfz{--shiki-light:#24292E;--shiki-default:#E1E4E8;--shiki-dark:#89DDFF}html pre.shiki code .seSrl, html code.shiki .seSrl{--shiki-light:#032F62;--shiki-default:#9ECBFF;--shiki-dark:#89DDFF}html pre.shiki code .sKpYG, html code.shiki .sKpYG{--shiki-light:#6F42C1;--shiki-default:#B392F0;--shiki-dark:#82AAFF}html pre.shiki code .swB56, html code.shiki .swB56{--shiki-light:#D73A49;--shiki-default:#F97583;--shiki-dark:#C792EA}html pre.shiki code .sNTR6, html code.shiki .sNTR6{--shiki-light:#E36209;--shiki-light-font-style:inherit;--shiki-default:#FFAB70;--shiki-default-font-style:inherit;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sDHlG, html code.shiki .sDHlG{--shiki-light:#24292E;--shiki-default:#E1E4E8;--shiki-dark:#F07178}html pre.shiki code .swWMF, html code.shiki .swWMF{--shiki-light:#005CC5;--shiki-default:#79B8FF;--shiki-dark:#FF9CAC}html pre.shiki code .s3Ny6, html code.shiki .s3Ny6{--shiki-light:#005CC5;--shiki-default:#79B8FF;--shiki-dark:#82AAFF}",{"title":1146,"searchDepth":1180,"depth":1180,"links":1914},[1915,1916,1917,1920,1921],{"id":629,"depth":1180,"text":630},{"id":747,"depth":1180,"text":748},{"id":827,"depth":1180,"text":706,"children":1918},[1919],{"id":1073,"depth":1200,"text":882},{"id":1135,"depth":1180,"text":1136},{"id":1374,"depth":1180,"text":1375,"children":1922},[1923],{"id":1741,"depth":1200,"text":1742},"The template's opt-in Playwright tests run each branch's production images in a throwaway stack and drive the site in Chromium. They gate every deploy. Run them locally and add your own.","md",{},{"title":593,"description":1924},"wxL-PiEnAyGh9NeLnUpGq3Ke9SpZmtIom48c2Wuam0Q",[1930,1932],{"title":589,"path":590,"stem":591,"description":1931,"children":-1},"Why a fast CWA page can score anywhere from 60 to 96 on mobile Lighthouse, how to read the result, and what a project should check first — third-party scripts, icons, images and its own front-end bundle.",{"title":597,"path":598,"stem":599,"description":1933,"children":-1},"When a project's functional test suite slows CI down — measure where the time goes, reset the database without rebuilding the schema, and shard the job by test.",1793565696360]