streamlit
1/**
2* Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2024)
3*
4* Licensed under the Apache License, Version 2.0 (the "License");
5* you may not use this file except in compliance with the License.
6* You may obtain a copy of the License at
7*
8* http://www.apache.org/licenses/LICENSE-2.0
9*
10* Unless required by applicable law or agreed to in writing, software
11* distributed under the License is distributed on an "AS IS" BASIS,
12* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13* See the License for the specific language governing permissions and
14* limitations under the License.
15*/
16
17// ***********************************************
18// This example commands.js shows you how to
19// create various custom commands and overwrite
20// existing commands.
21//
22// For more comprehensive examples of custom
23// commands please read more here:
24// https://on.cypress.io/custom-commands
25// ***********************************************
26//
27//
28// -- This is a parent command --
29// Cypress.Commands.add("login", (email, password) => { ... })
30//
31//
32// -- This is a child command --
33// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
34//
35//
36// -- This is a dual command --
37// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
38//
39//
40// -- This is will overwrite an existing command --
41// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
42
43import path from "path"44import * as _ from "lodash"45
46// https://github.com/palmerhq/cypress-image-snapshot#installation
47import { addMatchImageSnapshotCommand } from "cypress-image-snapshot/command"48import "cypress-file-upload"49
50/**
51* Returns an OS and device-pixel-ratio specific snapshot folder, e.g. <rootDir>/cypress/snapshots/darwin/2x
52* We use per-OS snapshots to account for rendering differences in fonts and UI widgets.
53* We use per-DPR snapshots to account for rendering differences in image dimensions.
54*/
55function getSnapshotFolder() {56const devicePixelRatio = Cypress.env("devicePixelRatio") || 257return path.join(58"cypress",59"snapshots",60Cypress.platform,61devicePixelRatio + "x"62)63}
64
65addMatchImageSnapshotCommand({66customSnapshotsDir: getSnapshotFolder(),67failureThreshold: 0.01, // Threshold for entire image68failureThresholdType: "percent", // Percent of image or number of pixels69})70
71Cypress.Commands.add("openSettings", () => {72cy.get("#MainMenu > button").click()73cy.get('[data-testid="main-menu-list"]').should("contain.text", "Settings")74cy.get('[data-testid="main-menu-list"]')75.contains("Settings")76.click({ force: true })77})78
79Cypress.Commands.add("changeTheme", theme => {80cy.openSettings()81cy.get('[data-baseweb="modal"] .stSelectbox').then(el => {82cy.wrap(el).find("input").click()83cy.get("li").contains(theme).click()84})85cy.get('[data-baseweb="modal"] [aria-label="Close"]').click()86})87
88/**
89* Normal usage:
90*
91* cy.get("my selector").first().matchImageSnapshot("my filename")
92*
93* This means the "subject" in the matchThemedSnapshots function will be the
94* result of cy.get("my selector").first(). However, in some cases the subject
95* detaches from the DOM when we change themes (this seems to happen with the
96* image in the staticfiles_app test, for example), causing Cypress to fail.
97* When that happens, you can fix the issue by passing a getSubject function
98* to this command to get the subject from the DOM again, like this:
99*
100* cy.get("body").matchImageSnapshot(
101* "my filename", {},
102* () => cy.get("my selector").first()
103* )
104*
105* Note that the example above uses cy.get("body") because that part of the
106* incantation doesn't actually matter. It just needs to exist.
107*/
108Cypress.Commands.add(109"matchThemedSnapshots",110{ prevSubject: true },111(subject, name, options, getSubject) => {112const testName = name || Cypress.mocha.getRunner().suite.ctx.test.title113const setStates = () => {114const { focus } = _.pick(options, ["focus"])115if (focus) {116cy.get(subject).within(() => {117cy.get(focus).focus()118})119}120}121
122if (!getSubject) {123getSubject = () => cy.wrap(subject)124}125
126// Get dark mode snapshot first. Taking light mode snapshot first127// for some reason ends up comparing dark with light128cy.changeTheme("Dark")129setStates()130getSubject().matchImageSnapshot(`${testName}-dark`, {131...options,132force: false,133})134
135// Revert back to light mode136cy.changeTheme("Light")137setStates()138getSubject().matchImageSnapshot(testName, { ...options, force: false })139cy.screenshot()140}141)
142
143// Calling trigger before capturing the snapshot forces Cypress to very Actionability.
144// https://docs.cypress.io/guides/core-concepts/interacting-with-elements.html#Actionability
145// This fixes the issue where snapshots are cutoff or the wrong element is captured.
146Cypress.Commands.overwrite(147"matchImageSnapshot",148(originalFn, subject, name, options) => {149cy.wrap(subject).trigger("blur", _.pick(options, ["force"]))150
151const headerHeight = 2.875 // In rem152const fontSizeMedium = 16 // In px153cy.get(subject).scrollIntoView({154offset: {155top: -1 * headerHeight * fontSizeMedium,156},157})158
159return originalFn(subject, name, options)160}161)
162
163Cypress.Commands.add("loadApp", (appUrl, timeout) => {164cy.visit(appUrl)165
166cy.waitForScriptFinish(timeout)167})168
169Cypress.Commands.add("waitForScriptFinish", (timeout = 20000) => {170// Wait until we know the script has started. We determine this by checking171// whether the app is in notRunning state. (The data-teststate attribute goes172// through the sequence "initial" -> "running" -> "notRunning")173cy.get("[data-testid='stApp'][data-teststate='notRunning']", {174timeout,175}).should("exist")176})177
178// Indexing into a list of elements produced by `cy.get()` may fail if not enough
179// elements are rendered, but this does not prompt cypress to retry the `get` call,
180// so the list will never update. This is a major cause of flakiness in tests.
181// The solution is to use `should` to wait for enough elements to be available first.
182// This is a convenience function for doing this automatically.
183Cypress.Commands.add("getIndexed", (selector, index) =>184cy
185.get(selector)186.should("have.length.at.least", index + 1)187.eq(index)188)
189
190// The header at the top of the page can sometimes interfere when we are
191// attempting to take snapshots. This command removes the problematic parts to
192// avoid this issue.
193Cypress.Commands.add("prepForElementSnapshots", () => {194// Look for the ribbon and if its found,195// make the ribbon decoration line disappear as it can occasionally get196// caught when a snapshot is taken.197cy.get(".stApp").then($body => {198if ($body.find("[data-testid='stDecoration']").length > 0) {199cy.get("[data-testid='stDecoration']").invoke("css", "display", "none")200}201})202
203// Similarly, the header styling can sometimes interfere with the snapshot204// for elements near the top of the page.205cy.get(".stApp > header").invoke("css", "background", "none")206cy.get(".stApp > header").invoke("css", "backdropFilter", "none")207})208
209// Allows the user to execute code within the iframe itself
210// This is useful for testing/changing examples of Streamlit embeddings
211Cypress.Commands.add(212"iframe",213{ prevSubject: "element" },214($iframe, callback = () => {}) => {215// For more info on targeting inside iframes refer to this GitHub issue:216// https://github.com/cypress-io/cypress/issues/136217cy.log("Getting iframe body")218
219return cy220.wrap($iframe)221.should(iframe => expect(iframe.contents().find("body")).to.exist)222.then(iframe => cy.wrap(iframe.contents().find("body")))223.within({}, callback)224}225)
226
227// Rerun the script by simulating the user pressing the 'r' key.
228Cypress.Commands.add("rerunScript", () => {229cy.get(".stApp [data-testid='stHeader']").trigger("keypress", {230keyCode: 82, // "r"231which: 82, // "r"232force: true,233})234})235
236Cypress.Commands.add("waitForRerun", () => {237cy.get("[data-testid='stStatusWidget']", { timeout: 10000 }).should("exist")238cy.get("[data-testid='stStatusWidget']", { timeout: 10000 }).should(239"not.exist"240)241})242
243// https://github.com/quasarframework/quasar/issues/2233
244// This error means that ResizeObserver was not able to deliver all observations within a single animation frame
245// It is benign (your site will not break).
246const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/247Cypress.on("uncaught:exception", err => {248/* returning false here prevents Cypress from failing the test */249if (resizeObserverLoopErrRe.test(err.message)) {250return false251}252})253