Cypress Fun

Yeah, unfortunately it ain't that simple 😅 Cypress, like you say, automatically retries things, and it already has a default timeout of 4s. The reason I have a bunch of cy.wait() (which isn't an ideal solution) is to wait for Foundry render animations to finish. When an element is rendered, Cypress will detect it and proceed with the test, but if it isn't fully visible, then that can lead to some weird behavior. That's why changing the timeout of cy.get() doesn't really do anything, as Cypress will detect the element anyway. I specifically want to wait for it to finish its rendering animation.
30 Replies
LukeAbby
LukeAbby•3y ago
Oh cool I think you could get around any button clicking with this code I hacked together by looking at the run HTTP requests:
const worldData = {
name: "bar",
title: "Bar",
system: "dnd5e"
}

async function createWorld() {
const createWorldJSON = await fetchJsonWithTimeout(
foundry.utils.getRoute("setup"),
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "createWorld",
// I assume you don't care about these being set.
background: "",
description: "",
nextSession: null,
...worldData
}),
}
);

handleError(createWorldJSON);
}

async function launchWorld() {
// The #setup-configuration form contains stuff like the setup screen credentials.
const formData = new FormDataExtended(
document.querySelector("form#setup-configuration")
);
formData.set("action", "launchWorld");
formData.set("world", worldData.name);

const launchWorldResponse = await fetchWithTimeout(
foundry.utils.getRoute("setup"),
{
method: "POST",
body: formData,
}
);

if (launchWorldResponse.redirected) {
window.location.href = launchWorldResponse.url;
}

const launchWorldJSON = await launchWorldResponse.json();
handleError(launchWorldJSON);

// This should never be able to run if we redirect pages.
throw new Error("Did not redirect to a new page!");
}

function handleError(responseJSON) {
if (typeof responseJSON.error !== "undefined") {
const error = new Error(game.i18n.localize(responseJSON.error));
error.stack = responseJSON.stack;

throw error;
}
}
const worldData = {
name: "bar",
title: "Bar",
system: "dnd5e"
}

async function createWorld() {
const createWorldJSON = await fetchJsonWithTimeout(
foundry.utils.getRoute("setup"),
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "createWorld",
// I assume you don't care about these being set.
background: "",
description: "",
nextSession: null,
...worldData
}),
}
);

handleError(createWorldJSON);
}

async function launchWorld() {
// The #setup-configuration form contains stuff like the setup screen credentials.
const formData = new FormDataExtended(
document.querySelector("form#setup-configuration")
);
formData.set("action", "launchWorld");
formData.set("world", worldData.name);

const launchWorldResponse = await fetchWithTimeout(
foundry.utils.getRoute("setup"),
{
method: "POST",
body: formData,
}
);

if (launchWorldResponse.redirected) {
window.location.href = launchWorldResponse.url;
}

const launchWorldJSON = await launchWorldResponse.json();
handleError(launchWorldJSON);

// This should never be able to run if we redirect pages.
throw new Error("Did not redirect to a new page!");
}

function handleError(responseJSON) {
if (typeof responseJSON.error !== "undefined") {
const error = new Error(game.i18n.localize(responseJSON.error));
error.stack = responseJSON.stack;

throw error;
}
}
I believe the Cypress built in functions would probably be: - document.querySelector("form#setup-configuration") -> cy.get("form#setup-configuration"); - window.location.href = launchWorldResponse.url; -> cy.visit(launchWorldResponse.url); - fetchWithTimeout/fetchJsonWithTimeout -> cy.request This of course will break if the requests to the backend ever change but I think it should be easy enough to update just by looking at the network tab and this way you don't have to click so many buttons. The UI might also update more not sure. @re4xn since asfik opening a thread doesn't ping
Re4XN
Re4XNOP•3y ago
Ah, that might just allow me to bypass the hangup at login 🤔 I'll test it out and see what happens!
LukeAbby
LukeAbby•3y ago
hmm I may switch to playwright, I saw JDW doing some work on it and I've already become unsatisfied with the lack of await for Cypress. maybe that can be filled in as Cypress does seem superior (?) not 100% sure Okay got an entire setup and teardown working. It takes about 13s though in the worst case unfortunately (when the server starts in the join screen). It's a mixture of HTTP requests and ui clicking. But I'm pretty excited! One thing I'm more than a little annoyed about though is for some reason something like:
before(() => {
cy.visit(x);
cy.get("...");
...
});
before(() => {
cy.visit(x);
cy.get("...");
...
});
doesn't work! But it totally seemed to work for you so I'm a bit confused. I had to refactor it like:
before(() => {
return cy.visit(x).get("...");
});
before(() => {
return cy.visit(x).get("...");
});
Granted this is within cypress/support/index.ts adjacent so maybe things work different? I mean I know stuff like cy.visit(x) is a promise so it made sense that I had to chain them but... idk other people don't seem to have to chain it to work right so it's confusing. Update: got stuff like
before(() => {
cy.visit(x);
cy.get("...");
...
});
before(() => {
cy.visit(x);
cy.get("...");
...
});
to work but for some reason now:
after(() => {
cy.get("...");
});
after(() => {
cy.get("...");
});
Is able to return no elements... I'm literally calling it exactly the same way in before and it works.
Re4XN
Re4XNOP•3y ago
I guess it's a difference of philosophy. I've used Cypress for a bit and haven't really run into a situation where I missed awaits. I actually strayed away from Playwright/Puppeteer because I couldn't be bothered to spam the await keyword everywhere xD
LukeAbby
LukeAbby•3y ago
I actually really only was so upset about it because I couldn't seem to do stuff in the form of cy.a(); cy.b() only in the form cy.a().then(() => cy.b()) but I managed to fix the bug causing that after realising no one was writing code like that in the examples I found I got into HUGE callback nesting when I couldn't get the first form to work haha so I didn't like it much
Re4XN
Re4XNOP•3y ago
Yeah, it takes a while to run unless you do it headless. My system setup takes 40s and I haven't even got to testing rolls 😬
LukeAbby
LukeAbby•3y ago
Well I think I might've cut off 27s then? Assuming that's not including setup for like... your Foundry system where you need to do clicks and stuff
Re4XN
Re4XNOP•3y ago
It does include that
LukeAbby
LukeAbby•3y ago
ah okay, well I think my script might speed things up some! but idk how much
Re4XN
Re4XNOP•3y ago
I'll have to take a look when I get back home xD What time is it over there? It seems our timezones are wildly different, I haven't been able to catch you to do voice, ahahah
LukeAbby
LukeAbby•3y ago
well my sleep schedule doesn't align with my timezone
LukeAbby
LukeAbby•3y ago
got this code so far for the setup
LukeAbby
LukeAbby•3y ago
I hook it up with just import "./globalSetup"; in cypress/support/index.ts. I've got yarn run cypress run --headless -b ... to work just fine for me btw do you know of anyway to anonymize some things within the cypress tests? I might have to turn off the .mp4 generation or something because it'll leak the admin key.
Re4XN
Re4XNOP•3y ago
I think you can do it by passing env variables to your run command and having that variable in Gitlab/GitHub secrets. Can't recall it off the top of my head, though
LukeAbby
LukeAbby•3y ago
Alright thanks, I'll look into that Wait so do you know of a way to easily do something like this: src/foo.js
export function toTest() {
return game.blah.xyz;
}
export function toTest() {
return game.blah.xyz;
}
cypress/integration/foundryvtt/foo.spec.js
import { toTest } from "../../../src/foo.js";

describe("...", () => {
it("...", () => {
expect(toTest()).to.equal(...);
});
});
import { toTest } from "../../../src/foo.js";

describe("...", () => {
it("...", () => {
expect(toTest()).to.equal(...);
});
});
Currently it'll bug out at the import because game doesn't exist on Cypress's window. Since it's being imported from a spec.js file you'd have to do cy.window().then(w=>...) to access the page's window. The argument might be that Cypress is for E2E testing only... but I would like to test my utility functions as well...
Re4XN
Re4XNOP•3y ago
I think you might be able to combine Cypress with something like Jest to get the full package of testing. I've never really used Cypress for anything other than E2E, so not sure if I can help in this regard 😅 But since Cypress runs in browser, I find it odd you can't access game 🤔
LukeAbby
LukeAbby•3y ago
well Cypress does run in browser and you can access game it just looks like this
cy.window().then(w => {
...
});
cy.window().then(w => {
...
});
because in a .spec.js file you don't have the real window but the functions I'm trying to import into .spec.js expect the real window
Re4XN
Re4XNOP•3y ago
Ah, I see the issue. No clue how to work around that though, sorry
LukeAbby
LukeAbby•3y ago
's fine was a longshot anyways oh I don't imagine you'll care but I added:
const enabledModules = Cypress.env("enabledModules") as string[];

window.game.settings.set("core", "moduleConfiguration", {
...window.game.settings.get("core", "moduleConfiguration"),
...enabledModules.reduce((allModules, module) => ({ ...allModules, [module]: true }), {}),
});
const enabledModules = Cypress.env("enabledModules") as string[];

window.game.settings.set("core", "moduleConfiguration", {
...window.game.settings.get("core", "moduleConfiguration"),
...enabledModules.reduce((allModules, module) => ({ ...allModules, [module]: true }), {}),
});
to joinWorld, in order to start some modules to the game. (has to be within cy.window().then((window) => ...)) I also found out that you can do this cy.get('[name=password]').type(password, {log: false})
Re4XN
Re4XNOP•3y ago
That's pretty dope. My system has a dependency on socketlib for one important functionality. But still, I think I'd prefer enabling the module the way a system user would, i.e. by navigating menus and clicking the checkboxes and buttons. It feels more in line with the whole E2E concept, I think 😅
LukeAbby
LukeAbby•3y ago
Fair enough! I just don't wanna E2E test Foundry itself but reasonable minds differ.
Re4XN
Re4XNOP•3y ago
If you want to hop in voice I'm available now. I still haven't managed to get things to run headless.
LukeAbby
LukeAbby•3y ago
Alright I'm in General
Re4XN
Re4XNOP•3y ago
Power went out I might be a few mins
LukeAbby
LukeAbby•3y ago
rightyo
LukeAbby
LukeAbby•3y ago
npm
cypress-wait-until
A waiting plugin for Cypress. Latest version: 1.7.2, last published: 7 months ago. Start using cypress-wait-until in your project by running npm i cypress-wait-until. There are 23 other projects in the npm registry using cypress-wait-until.
LukeAbby
LukeAbby•3y ago
It's possible you were running into this:
Error: CypressError: Cypress detected a cross origin error happened on page load:

> Blocked a frame with origin "http://localhost:30000" from accessing a cross-origin frame.

Before the page load, you were bound to the origin policy:

> http://localhost:30000

A cross origin error happens when your application navigates to a new URL which does not match the origin policy above.

A new URL does not match the origin policy if the 'protocol', 'port' (if specified), and/or 'host' (unless of the same superdomain) are different.

Cypress does not allow you to navigate to a different origin URL within a single test.

You may need to restructure some of your test code to avoid this problem.
Error: CypressError: Cypress detected a cross origin error happened on page load:

> Blocked a frame with origin "http://localhost:30000" from accessing a cross-origin frame.

Before the page load, you were bound to the origin policy:

> http://localhost:30000

A cross origin error happens when your application navigates to a new URL which does not match the origin policy above.

A new URL does not match the origin policy if the 'protocol', 'port' (if specified), and/or 'host' (unless of the same superdomain) are different.

Cypress does not allow you to navigate to a different origin URL within a single test.

You may need to restructure some of your test code to avoid this problem.
Cut the runtime down to ~5s
Re4XN
Re4XNOP•3y ago
Hmm, that makes sense, but I don't think I'm changing origins within my tests 🤔 If that were the case, then the same would happen when not running headless, right?
LukeAbby
LukeAbby•3y ago
I wasn’t actually changing origins myself I just got that to show up Something nice I found out:
cy.window().its("game.ready").should("eq", true);
cy.window().its("game.ready").should("eq", true);
Can replace my ugly waitUntil and stuff. Have you had problems with hardware acceleration? It doesn't seem to be on by default and it's a mild inconvenience.
Re4XN
Re4XNOP•3y ago
Yeah, I have. If I run the Electron browser then Foundry doesn't pop the warning, so I assume it is enabled by default on that browser. At least, I haven't had any issues on Electron, as opposed to Edge, for example. I only ever have these issues on headless mode, though. When running the application I can test all browsers with no issues.
Want results from more Discord servers?
Add your server