[{"data":1,"prerenderedAt":726},["ShallowReactive",2],{"SiteFooter_cLEacilcm59lTjFJjLY6mXNWLL0ETtzOIiZwIHgDpy0":3,"doc-\u002Fdocs\u002Fextending\u002Fbrowser-as-a-base":17,"docs-nav":629,"nav-compare":714,"editor-sfq2bi":724},["Island",4],{"key":5,"result":6},"SiteFooter_cLEacilcm59lTjFJjLY6mXNWLL0ETtzOIiZwIHgDpy0",{"head":7},{"style":8},[9,11,13,15],{"innerHTML":10},".site-footer[data-v-8bb1d4ae]{border-top:1px solid var(--line);background:var(--chrome);padding:var(--sp-8) var(--gutter) var(--sp-10);gap:var(--sp-6);flex-direction:column;display:flex}.links[data-v-8bb1d4ae]{gap:var(--sp-4);grid-template-columns:repeat(auto-fit,minmax(15rem,1fr));max-width:62rem;display:grid}.links a[data-v-8bb1d4ae]{align-items:flex-start;gap:var(--sp-3);padding:var(--sp-3);border-bottom:1px solid var(--line);border-radius:var(--radius-sm);background:var(--panel);color:var(--ink);border:1px solid var(--line);display:flex}.links a[data-v-8bb1d4ae]:focus-visible,.links a[data-v-8bb1d4ae]:hover{border-color:var(--line-strong)}.links[data-v-8bb1d4ae] .mark,.links[data-v-8bb1d4ae] .rf-mark{width:1.35rem;height:1.35rem;color:var(--dim);flex:none;margin-top:.1rem}.links a[data-v-8bb1d4ae]:hover .mark,.links a[data-v-8bb1d4ae]:hover .rf-mark{color:var(--red)}.links span[data-v-8bb1d4ae]{flex-direction:column;gap:2px;min-width:0;display:flex}.links b[data-v-8bb1d4ae]{font-family:var(--font-display);font-size:.85rem;font-weight:400}.links small[data-v-8bb1d4ae]{color:var(--faint);font-size:.78rem}.colophon[data-v-8bb1d4ae]{color:var(--faint);margin:0;font-size:.75rem}@supports (corner-shape:bevel){.links a[data-v-8bb1d4ae]{corner-shape:bevel}}",{"innerHTML":12},".mark[data-v-cc0d96a5]{vertical-align:-.125em;flex:none;width:1em;height:1em}",{"innerHTML":14},".mark[data-v-26f5673d]{vertical-align:-.125em;flex:none;width:1em;height:1em}",{"innerHTML":16},".rf-mark[data-v-26317b3e]{fill:currentColor;stroke:none;flex:none}",{"id":18,"title":19,"body":20,"description":621,"extension":622,"meta":623,"navigation":217,"order":227,"path":624,"section":625,"seo":626,"stem":627,"__hash__":628},"docs\u002Fdocs\u002Fextending\u002Fbrowser-as-a-base.md","Browser as a base",{"type":21,"value":22,"toc":606},"minimark",[23,37,45,50,56,61,64,68,88,92,100,141,153,167,170,174,185,188,290,306,313,320,325,328,332,339,362,366,391,408,412,427,430,452,462,468,475,478,486,499,517,523,542,546,552,555,559,569,573,591,602],[24,25,26,27,32,33],"p",{},"On the ",[28,29,31],"a",{"href":30},"\u002Fdocs\u002Fextending\u002Fpython-libraries","previous page"," Robot Framework imports\nBrowser and your library sits alongside it. This page is the other arrangement:\n",[34,35,36],"strong",{},"Robot Framework imports only your library, and your library constructs\nBrowser.",[24,38,39,40,44],{},"People arrive here when they are replacing the ",[41,42,43],"code",{},"Browser"," import and a pile of\nresource-file keywords with a library of their own — a domain vocabulary rather\nthan a browser vocabulary, with failure handling they control.",[46,47,49],"h2",{"id":48},"one-question-decides-everything","One question decides everything",[24,51,52,53],{},"Not how advanced your library is. ",[34,54,55],{},"Who imports Browser.",[57,58],"doc-table",{":head":59,":rows":60},"[\"\",\"Alongside Browser\",\"Browser as a base\"]","[[\"Robot Framework imports\",\"Browser **and** your library\",\"only your library\"],[\"Who constructs `Browser()`\",\"Robot Framework\",\"your library\"],[\"How your library reaches it\",\"`BuiltIn().get_library_instance(\\\"Browser\\\")`\",\"it holds the instance it made\"],[\"Browser's listener is registered\",\"automatically\",\"only if you register it\"]]",[24,62,63],{},"Everything below follows from owning the instance, and it is not a list of\nlosses. It is three tiers.",[57,65],{":head":66,":rows":67},"[\"Tier\",\"What is in it\"]","[[\"**Free** — because Robot Framework is running\",\"argument conversion, `outputdir`, the `validate` and `then` operators\"],[\"**One line** — register Browser as a listener\",\"automatic closing, scope settings, the Robot Framework context on the Node side\"],[\"**Yours to build**\",\"`run_on_failure`, and a little of the tracing\"]]",[69,70,72,78],"doc-note",{"kind":71},"warning",[24,73,74,77],{},[34,75,76],{},"Importing Browser anywhere in the suite puts you back in the arrangement on\nthe previous page"," — including from a resource file, and including when your\nlibrary owns an instance too. Robot Framework then has its own Browser, and the\nproperties described on this page stop being true of your run without anything\nfailing.",[24,79,80,81,83,84,87],{},"The library's acceptance suite guards this rather than trusting review: it fails\nthe run if any keyword owned by ",[41,82,43],{}," appears in the child suite's\n",[41,85,86],{},"output.xml",".",[46,89,91],{"id":90},"free-because-robot-framework-is-running","Free — because Robot Framework is running",[24,93,94,95,99],{},"These hang off ",[96,97,98],"em",{},"a Robot Framework run existing at all",", not off who imported\nBrowser. They are true in this arrangement whether or not you go on to register\nthe listener.",[24,101,102,106,107,110,113,114,117,118,121,122,125,126,129,130,133,134,136,137,140],{},[103,104],"since",{"version":105},"20.4.0"," ",[34,108,109],{},"Arguments convert from plain Python values.",[41,111,112],{},"browser.click(\"id=x\", \"middle\")",", ",[41,115,116],{},"\"2 seconds\""," into a ",[41,119,120],{},"timedelta",",\n",[41,123,124],{},"\"validate\""," into an ",[41,127,128],{},"AssertionOperator"," — the same converters Robot Framework\nuses, and a Python ",[41,131,132],{},"None"," stays ",[41,135,132],{},". The four rules are on\n",[28,138,139],{"href":30},"the previous page"," and they do not change\nhere.",[24,142,143,152],{},[34,144,145,148,149,87],{},[41,146,147],{},"browser.outputdir"," is Robot Framework's ",[41,150,151],{},"${OUTPUTDIR}"," Screenshots,\nvideos, traces and the Playwright log land in the run's output directory,\nexactly as they do for any other suite.",[24,154,155,166],{},[34,156,157,158,161,162,165],{},"The ",[41,159,160],{},"validate"," and ",[41,163,164],{},"then"," assertion operators execute."," They need an\nexecution context, and there is one.",[24,168,169],{},"That last pair is worth stating plainly because it has been published wrongly\nbefore: these are properties of a Robot Framework run, not of Browser having\nbeen imported by it.",[46,171,173],{"id":172},"one-line-registering-the-listener","One line — registering the listener",[24,175,176,177,180,181,184],{},"Everything in that middle tier is implemented in Browser's listener methods.\nRobot Framework only looks for ",[41,178,179],{},"ROBOT_LIBRARY_LISTENER"," on libraries ",[34,182,183],{},"it","\nimported, so when your library owns the instance, nobody tells Browser that a\ntest started or ended.",[24,186,187],{},"Hand it over in your constructor:",[189,190,196],"pre",{"className":191,"code":192,"filename":193,"language":194,"meta":195,"style":195},"language-python shiki shiki-themes github-light github-dark","class MyLibraryB:\n    \"\"\"Browser used as a base, with this library owning the instance.\"\"\"\n\n    ROBOT_LIBRARY_SCOPE = \"GLOBAL\"\n    # Robot Framework resolves the listener API version of every listener\n    # separately. Browser declares version 2, but this library would default to\n    # version 3, which has different method signatures.\n    ROBOT_LISTENER_API_VERSION = 2\n\n    def __init__(self):\n        self._browser = Browser(enable_playwright_debug=True)\n        # Robot Framework accepts a list of listeners and calls Browser's\n        # listener methods itself. Without this line Browser gets no suite and\n        # test events, so automatic closing and scope settings do not work.\n        self.ROBOT_LIBRARY_LISTENER = [self, self._browser]\n","MyLibraryB.py","python","",[41,197,198,206,212,219,225,231,237,243,249,254,260,266,272,278,284],{"__ignoreMap":195},[199,200,203],"span",{"class":201,"line":202},"line",1,[199,204,205],{},"class MyLibraryB:\n",[199,207,209],{"class":201,"line":208},2,[199,210,211],{},"    \"\"\"Browser used as a base, with this library owning the instance.\"\"\"\n",[199,213,215],{"class":201,"line":214},3,[199,216,218],{"emptyLinePlaceholder":217},true,"\n",[199,220,222],{"class":201,"line":221},4,[199,223,224],{},"    ROBOT_LIBRARY_SCOPE = \"GLOBAL\"\n",[199,226,228],{"class":201,"line":227},5,[199,229,230],{},"    # Robot Framework resolves the listener API version of every listener\n",[199,232,234],{"class":201,"line":233},6,[199,235,236],{},"    # separately. Browser declares version 2, but this library would default to\n",[199,238,240],{"class":201,"line":239},7,[199,241,242],{},"    # version 3, which has different method signatures.\n",[199,244,246],{"class":201,"line":245},8,[199,247,248],{},"    ROBOT_LISTENER_API_VERSION = 2\n",[199,250,252],{"class":201,"line":251},9,[199,253,218],{"emptyLinePlaceholder":217},[199,255,257],{"class":201,"line":256},10,[199,258,259],{},"    def __init__(self):\n",[199,261,263],{"class":201,"line":262},11,[199,264,265],{},"        self._browser = Browser(enable_playwright_debug=True)\n",[199,267,269],{"class":201,"line":268},12,[199,270,271],{},"        # Robot Framework accepts a list of listeners and calls Browser's\n",[199,273,275],{"class":201,"line":274},13,[199,276,277],{},"        # listener methods itself. Without this line Browser gets no suite and\n",[199,279,281],{"class":201,"line":280},14,[199,282,283],{},"        # test events, so automatic closing and scope settings do not work.\n",[199,285,287],{"class":201,"line":286},15,[199,288,289],{},"        self.ROBOT_LIBRARY_LISTENER = [self, self._browser]\n",[24,291,292,295,296,298,299,302,303,87],{},[34,293,294],{},"This is a supported public contract of Browser, not a workaround."," Two Robot\nFramework behaviours make it work: ",[41,297,179],{}," accepts a ",[96,300,301],{},"list",", so\nyour library and Browser can both be listeners; and for a library listener Robot\nFramework also looks for method names with a leading underscore, which is how it\nfinds ",[41,304,305],{},"Browser._start_suite",[24,307,308,309,312],{},"That leading underscore is not a private-API marker. It is the convention that\nstops a listener method from also becoming a keyword — Robot Framework's library\nAPI skips names beginning with ",[41,310,311],{},"_",", and the library-listener lookup puts the\nunderscore back.",[24,314,315,316,319],{},"You can write ",[41,317,318],{},"ROBOT_LIBRARY_LISTENER = self._browser"," instead, and Browser\nstill gets its events. The list form is what lets you be a listener as well.",[321,322,324],"h3",{"id":323},"what-the-line-buys","What the line buys",[24,326,327],{},"The library's tests run the same suite against two libraries that differ only in\nwhether they hand Browser that registration. The difference is measured, not\nargued:",[57,329],{":head":330,":rows":331},"[\"After three tests that each open a page\",\"With the line\",\"Without it\"]","[[\"Pages still open\",\"1\",\"3\"],[\"A `scope=Test` browser timeout, in the next test\",\"reverted to `10 seconds`\",\"still `3 seconds`\"],[\"Node-side log records carrying the Robot Framework test name\",\"73\",\"0\"]]",[24,333,334,335,338],{},"So the line buys three things: pages and contexts opened in a test are closed at\nits end, a scoped setting such as ",[41,336,337],{},"set_browser_timeout(…, \"Test\")"," reverts when\nthat test ends, and the Robot Framework suite and test names reach the Node side,\nwhere Playwright's own log and traces carry them.",[69,340,341],{"kind":71},[24,342,343,350,351,354,355,358,359,87],{},[34,344,345,346,349],{},"Without the listener, ",[41,347,348],{},"scope=Test"," fails silently."," It is not an error and\nthere is no warning: Browser never learned that a suite started, so it quietly\ndowngrades the setting to ",[41,352,353],{},"Global"," and it leaks into every later test. That is\nthe second row of the table above — the timeout is still ",[41,356,357],{},"3 seconds"," in the next\ntest rather than back at ",[41,360,361],{},"10",[321,363,365],{"id":364},"two-traps-in-the-registration","Two traps in the registration",[24,367,368,374,375,378,379,382,383,386,387,390],{},[34,369,370,371,87],{},"Declare ",[41,372,373],{},"ROBOT_LISTENER_API_VERSION = 2"," Robot Framework resolves the\nlistener API version ",[96,376,377],{},"per listener",", not per registration. In\n",[41,380,381],{},"[self, self._browser]",", Browser gets version 2 from its own class attribute\nwhile your library defaults to version 3 — a different set of method signatures.\nCopy a version-2 signature such as ",[41,384,385],{},"_end_keyword(self, name, attrs)"," into a\nlibrary that has not declared the version and you get a ",[41,388,389],{},"TypeError"," at run time\nthat does not say why.",[24,392,393,399,400,403,404,407],{},[34,394,395,396,87],{},"Give your library ",[41,397,398],{},"ROBOT_LIBRARY_SCOPE = \"GLOBAL\""," Browser is ",[41,401,402],{},"GLOBAL"," and\nkeeps state at class level, shared by every instance in the process. If Robot\nFramework constructs your library once per suite or per test, each new\n",[41,405,406],{},"Browser()"," you make joins the state the previous one left behind.",[46,409,411],{"id":410},"yours-to-build","Yours to build",[24,413,414,415,418,419,422,423,426],{},"One code path is not recovered by the listener, in either arrangement:\n",[41,416,417],{},"Browser.run_keyword",", the dynamic library API method Robot Framework calls to\nexecute a Browser keyword. Robot Framework calls it only on the library it\nimported itself, and registering a listener is not importing a library. In this\narrangement it calls ",[41,420,421],{},"run_keyword"," on ",[96,424,425],{},"your"," library, never on Browser.",[24,428,429],{},"What lives in there:",[431,432,433,442],"ul",{},[434,435,436,441],"li",{},[34,437,438],{},[41,439,440],{},"run_on_failure"," — no screenshot on a failing Browser call, listener or\nnot. This is the one that matters, and the recipes below are how you get it\nback.",[434,443,444,445,448,449,87],{},"Trace groups in the non-default ",[41,446,447],{},"TracingGroupMode.Browser",", Browser's\nrewriting of error messages, and ",[41,450,451],{},"pause_on_failure",[24,453,454,455,106,458,461],{},"Trace groups in the ",[34,456,457],{},"default",[41,459,460],{},"TracingGroupMode.Full"," are not in that list.\nThey are opened and closed by listener methods, and a library listener receives\nevents for every keyword in the run, so registering the listener gets you a\ntrace group per Robot Framework keyword.",[69,463,465],{"kind":464},"aside",[24,466,467],{},"No acceptance test covers that last paragraph, and that is deliberate: the only\nobservable is the Playwright trace file's own format, which is internal and not\nsomething this project should pin a test to. It is stated from the mechanism in\nthe library's source, because the opposite — \"trace groups never work when you\nown the instance\" — has been published before and is wrong.",[321,469,471,472,474],{"id":470},"recovering-run_on_failure-yourself","Recovering ",[41,473,440],{}," yourself",[24,476,477],{},"Three shapes, and no single correct one. Pick by the size of the library.",[24,479,480,483,484,87],{},[34,481,482],{},"A decorator",", best for a small library. It wraps the keywords you choose,\ntakes the screenshot and re-raises. It is the one shape the library's tests\ncover, and it is written out in full on\n",[28,485,139],{"href":30},[24,487,488,494,495,498],{},[34,489,490,491],{},"Your own ",[41,492,493],{},"_end_keyword",", nearly free here — your library is already a\nlistener, because that is how you registered Browser. Take a screenshot when\n",[41,496,497],{},"attrs[\"status\"] == \"FAIL\"",". Two things to know:",[431,500,501,508],{},[434,502,503,504,507],{},"A library listener is told about ",[34,505,506],{},"every"," keyword in the run, not only its\nown library's. Browser's own listener has to check the owning library name to\ntell its keywords from everybody else's, and so will yours.",[434,509,510,512,513,516],{},[41,511,385],{}," is a listener API version 2 signature, so\nthis is the same ",[41,514,515],{},"ROBOT_LISTENER_API_VERSION"," trap as above.",[24,518,519,520,522],{},"The granularity is different from the decorator's: a listener sees ",[96,521,425],{},"\nkeyword fail, not the individual Browser call inside it. That is usually what\nyou want in a report.",[24,524,525,528,529,535,536,538,539,541],{},[34,526,527],{},"A dynamic library",", optionally through\n",[28,530,534],{"href":531,"rel":532},"https:\u002F\u002Fgithub.com\u002Frobotframework\u002FPythonLibCore",[533],"nofollow","PythonLibCore",", for a large\none. Implement ",[41,537,421],{}," yourself and put the failure handling in it: every\nkeyword is covered by construction and nothing has to be remembered per keyword.\nBrowser's own ",[41,540,417],{}," is the worked reference — try, delegate, and\non failure call the failure keyword before re-raising.",[46,543,545],{"id":544},"the-log-gets-shallower-here-too","The log gets shallower here too",[24,547,548,549,551],{},"A Browser keyword called from Python is not a keyword in ",[41,550,86],{}," — Robot\nFramework never saw a keyword call — so everything Browser logs appears inside\nthe keyword of your library that made the call. That is true of both\narrangements, and here it is true of every Browser call you make, because they\nall come from your library.",[24,553,554],{},"The detailed Robot Framework log, one row per Browser keyword, becomes your own\nkeyword's log. Log what a reader of the report needs, because the automatic\ndetail is gone.",[46,556,558],{"id":557},"cleaning-up","Cleaning up",[24,560,561,562,565,566,568],{},"Nothing leaks. Browser registers an ",[41,563,564],{},"atexit"," hook when it starts the Node\nprocess, so the process tree goes down when the interpreter exits whether or not\na listener is registered. A suite teardown that closes the browser is not\nrequired for cleanliness — it is worth having only for the case where something\nkills the run with a signal and ",[41,567,564],{}," never runs.",[46,570,572],{"id":571},"where-these-examples-come-from","Where these examples come from",[24,574,575,576,579,580,582,583,586,587,590],{},"Every labelled block on this page is quoted from a real file, committed in this\nrepository under ",[41,577,578],{},"examples\u002Fpython-extension\u002F",", and a test fails the build if a\nquote stops matching it. ",[41,581,193],{}," is a complete worked library for this\npage and ",[41,584,585],{},"context_b.robot"," is the suite that exercises it.\n",[41,588,589],{},"MyLibraryB_no_listener.py"," is the same library without that registration, and\nthe difference between those two runs is the table above.",[24,592,593,594,601],{},"Those files are a copy. They run as acceptance tests in the library itself, in\n",[28,595,598],{"href":596,"rel":597},"https:\u002F\u002Fgithub.com\u002FMarketSquare\u002Frobotframework-browser\u002Ftree\u002Fmain\u002Fatest\u002Ftest\u002F13_Python_Extension",[533],[41,599,600],{},"atest\u002Ftest\u002F13_Python_Extension\u002F",",\nwhich is where a change to them belongs.",[603,604,605],"style",{},"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);}",{"title":195,"searchDepth":208,"depth":208,"links":607},[608,609,610,614,618,619,620],{"id":48,"depth":208,"text":49},{"id":90,"depth":208,"text":91},{"id":172,"depth":208,"text":173,"children":611},[612,613],{"id":323,"depth":214,"text":324},{"id":364,"depth":214,"text":365},{"id":410,"depth":208,"text":411,"children":615},[616],{"id":470,"depth":214,"text":617},"Recovering run_on_failure yourself",{"id":544,"depth":208,"text":545},{"id":557,"depth":208,"text":558},{"id":571,"depth":208,"text":572},"Own the Browser instance in your own library — what you get free, what one line restores, and what you build yourself.","md",{},"\u002Fdocs\u002Fextending\u002Fbrowser-as-a-base","extending",{"title":19,"description":621},"docs\u002Fextending\u002Fbrowser-as-a-base","7E74w54ZBd0D76kQjsRwmzVr5S9RXDNBkZUSCoT6zjQ",[630,635,639,643,647,651,655,656,660,664,667,671,675,680,684,688,692,697,701,705,709],{"path":631,"title":632,"description":633,"section":634,"order":227},"\u002Fdocs\u002Fconcepts\u002Farchitecture","How Browser works","Python, a Node process and Playwright — what runs where, and which installation to choose.","concepts",{"path":636,"title":637,"description":638,"section":634,"order":214},"\u002Fdocs\u002Fconcepts\u002Fassertions","Assertions","Almost every getter can assert, every assertion retries, and each return type allows different operators. The complete AssertionEngine reference.",{"path":640,"title":641,"description":642,"section":634,"order":202},"\u002Fdocs\u002Fconcepts\u002Fbrowser-context-page","Browser, context and page","The three layers Playwright is built on — and why a new context is the cheapest clean slate you will ever get.",{"path":644,"title":645,"description":646,"section":634,"order":233},"\u002Fdocs\u002Fconcepts\u002Flogging","Logging and debugging","Four places Browser tells you what happened — log.html, the Playwright log, traces and coverage.",{"path":648,"title":649,"description":650,"section":634,"order":208},"\u002Fdocs\u002Fconcepts\u002Fselectors","Finding elements","Which selector strategy to reach for, why the order matters, and how to chain across iframes and shadow DOM.",{"path":652,"title":653,"description":654,"section":634,"order":221},"\u002Fdocs\u002Fconcepts\u002Fwaiting","Waiting and promises","Most waiting is already done for you. This is the rest of it — Wait For Condition, and how to run keywords in parallel with Promise To.",{"path":624,"title":19,"description":621,"section":625,"order":227},{"path":657,"title":658,"description":659,"section":625,"order":208},"\u002Fdocs\u002Fextending\u002Fjavascript-basics","Just enough JavaScript","The JavaScript you need to write extensions and Evaluate JavaScript, for people who write Python.",{"path":661,"title":662,"description":663,"section":625,"order":214},"\u002Fdocs\u002Fextending\u002Fjavascript-extensions","JavaScript extensions","Add keywords that run on the Node side with full Playwright access — and attach a debugger to them.",{"path":30,"title":665,"description":666,"section":625,"order":221},"Your own Python library","Move business logic out of Robot Framework and keep Browser exactly as it is.",{"path":668,"title":669,"description":670,"section":625,"order":202},"\u002Fdocs\u002Fextending\u002Fpython-plugins","Python plugins","Add or replace keywords from Python, with access to the library's own API and to the Node side.",{"path":672,"title":673,"description":674,"section":625,"order":233},"\u002Fdocs\u002Fextending\u002Ftranslations","Translating keywords","Using a translation package to get keyword names and documentation in your language, and writing one for a language nobody has covered yet.",{"path":676,"title":677,"description":678,"section":679,"order":221},"\u002Fdocs\u002Fmobile\u002Fdevice-list","Every device descriptor","The full list of names Get Device accepts, with the viewport, pixel ratio, touch and engine each one sets — filterable and sortable.","mobile",{"path":681,"title":682,"description":683,"section":679,"order":208},"\u002Fdocs\u002Fmobile\u002Fdevices","Emulating a device","Device descriptors, what the six or seven properties they set actually change, and the one that emulation cannot give you.",{"path":685,"title":686,"description":687,"section":679,"order":202},"\u002Fdocs\u002Fmobile\u002Fresponsive","Testing responsive pages","Why a browser at a phone-sized viewport tests responsiveness properly, what emulation cannot tell you, and where real devices actually belong.",{"path":689,"title":690,"description":691,"section":679,"order":214},"\u002Fdocs\u002Fmobile\u002Ftouch","Touch, permissions and location","Tap and why it needs hasTouch, granting camera or notifications without a dialog, faking where the user is, and the gestures Browser has no keyword for.",{"path":693,"title":694,"description":695,"section":696,"order":214},"\u002Fdocs\u002Foperations\u002Fdocker","Running in Docker","The published Browser image, the four docker run flags that matter, and why running as root breaks Chromium.","operations",{"path":698,"title":699,"description":700,"section":696,"order":221},"\u002Fdocs\u002Foperations\u002Fdocker-images","Building your own image","Extending the published image with your dependencies, the version lock between Browser and Playwright, and using it as a CI job container.",{"path":702,"title":703,"description":704,"section":696,"order":208},"\u002Fdocs\u002Foperations\u002Fenvironment-variables","Environment variables","The variables that change how Browser behaves, which of them are safe in production, and the import parameters that should usually be preferred.",{"path":706,"title":707,"description":708,"section":696,"order":202},"\u002Fdocs\u002Foperations\u002Fnode-process","The Node process","Browser starts a Node process to talk to Playwright. What that costs, how to share one across parallel runs, and how to pass it Node flags.",{"path":710,"title":711,"description":712,"section":713,"order":202},"\u002Fdocs\u002Fstart\u002Fgetting-started","Getting started","Install Browser, write a first test, and run it.","start",[715,718,721],{"to":716,"label":717},"\u002Fwhy\u002Fvs-cypress","vs Cypress",{"to":719,"label":720},"\u002Fwhy\u002Fvs-playwright","vs Playwright Test",{"to":722,"label":723},"\u002Fwhy\u002Fvs-seleniumlibrary","vs SeleniumLibrary",[725],"\u003Cspan class=\"line\">\u003Cspan style=\"color:#2BD9CC\">class\u003C\u002Fspan>\u003Cspan style=\"color:#2BD9CC\"> MyLibraryB\u003C\u002Fspan>\u003Cspan style=\"color:#A9B4BA\">:\u003C\u002Fspan>\u003C\u002Fspan>\u003Cspan class=\"line\">\u003Cspan style=\"color:#A9B4BA\">    \"\"\"\u003C\u002Fspan>\u003Cspan style=\"color:#E0D3A3\">Browser used as a base, with this library owning the instance.\u003C\u002Fspan>\u003Cspan style=\"color:#A9B4BA\">\"\"\"\u003C\u002Fspan>\u003C\u002Fspan>\u003Cspan class=\"line\">\u003C\u002Fspan>\u003Cspan class=\"line\">\u003Cspan style=\"color:#E2E1D7\">    ROBOT_LIBRARY_SCOPE \u003C\u002Fspan>\u003Cspan style=\"color:#FF6B5E\">=\u003C\u002Fspan>\u003Cspan style=\"color:#A9B4BA\"> \"\u003C\u002Fspan>\u003Cspan style=\"color:#E0D3A3\">GLOBAL\u003C\u002Fspan>\u003Cspan style=\"color:#A9B4BA\">\"\u003C\u002Fspan>\u003C\u002Fspan>\u003Cspan class=\"line\">\u003Cspan style=\"color:#96A2A8;font-style:italic\">    # Robot Framework resolves the listener API version of every listener\u003C\u002Fspan>\u003C\u002Fspan>\u003Cspan class=\"line\">\u003Cspan style=\"color:#96A2A8;font-style:italic\">    # separately. Browser declares version 2, but this library would default to\u003C\u002Fspan>\u003C\u002Fspan>\u003Cspan class=\"line\">\u003Cspan style=\"color:#96A2A8;font-style:italic\">    # version 3, which has different method signatures.\u003C\u002Fspan>\u003C\u002Fspan>\u003Cspan class=\"line\">\u003Cspan style=\"color:#E2E1D7\">    ROBOT_LISTENER_API_VERSION \u003C\u002Fspan>\u003Cspan style=\"color:#FF6B5E\">=\u003C\u002Fspan>\u003Cspan style=\"color:#FFB86B\"> 2\u003C\u002Fspan>\u003C\u002Fspan>\u003Cspan class=\"line\">\u003C\u002Fspan>\u003Cspan class=\"line\">\u003Cspan style=\"color:#2BD9CC\">    def\u003C\u002Fspan>\u003Cspan style=\"color:#2BD9CC\"> __init__\u003C\u002Fspan>\u003Cspan style=\"color:#A9B4BA\">(\u003C\u002Fspan>\u003Cspan style=\"color:#E2E1D7\">self\u003C\u002Fspan>\u003Cspan style=\"color:#A9B4BA\">):\u003C\u002Fspan>\u003C\u002Fspan>\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFB86B\">        self\u003C\u002Fspan>\u003Cspan style=\"color:#A9B4BA\">.\u003C\u002Fspan>\u003Cspan style=\"color:#E2E1D7\">_browser \u003C\u002Fspan>\u003Cspan style=\"color:#FF6B5E\">=\u003C\u002Fspan>\u003Cspan style=\"color:#2BD9CC\"> Browser\u003C\u002Fspan>\u003Cspan style=\"color:#A9B4BA\">(\u003C\u002Fspan>\u003Cspan style=\"color:#E2E1D7\">enable_playwright_debug\u003C\u002Fspan>\u003Cspan style=\"color:#FF6B5E\">=\u003C\u002Fspan>\u003Cspan style=\"color:#FFB86B\">True\u003C\u002Fspan>\u003Cspan style=\"color:#A9B4BA\">)\u003C\u002Fspan>\u003C\u002Fspan>\u003Cspan class=\"line\">\u003Cspan style=\"color:#96A2A8;font-style:italic\">        # Robot Framework accepts a list of listeners and calls Browser's\u003C\u002Fspan>\u003C\u002Fspan>\u003Cspan class=\"line\">\u003Cspan style=\"color:#96A2A8;font-style:italic\">        # listener methods itself. Without this line Browser gets no suite and\u003C\u002Fspan>\u003C\u002Fspan>\u003Cspan class=\"line\">\u003Cspan style=\"color:#96A2A8;font-style:italic\">        # test events, so automatic closing and scope settings do not work.\u003C\u002Fspan>\u003C\u002Fspan>\u003Cspan class=\"line\">\u003Cspan style=\"color:#FFB86B\">        self\u003C\u002Fspan>\u003Cspan style=\"color:#A9B4BA\">.\u003C\u002Fspan>\u003Cspan style=\"color:#E2E1D7\">ROBOT_LIBRARY_LISTENER \u003C\u002Fspan>\u003Cspan style=\"color:#FF6B5E\">=\u003C\u002Fspan>\u003Cspan style=\"color:#A9B4BA\"> [\u003C\u002Fspan>\u003Cspan style=\"color:#FFB86B\">self\u003C\u002Fspan>\u003Cspan style=\"color:#A9B4BA\">,\u003C\u002Fspan>\u003Cspan style=\"color:#FFB86B\"> self\u003C\u002Fspan>\u003Cspan style=\"color:#A9B4BA\">.\u003C\u002Fspan>\u003Cspan style=\"color:#E2E1D7\">_browser\u003C\u002Fspan>\u003Cspan style=\"color:#A9B4BA\">]\u003C\u002Fspan>\u003C\u002Fspan>",1787137745018]