Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(commands): laid out groundwork for state tracking #210

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from

Conversation

Andarist
Copy link
Collaborator

@Andarist Andarist commented Dec 27, 2021

This builds on top of #201

@@ -0,0 +1,36 @@
import {mouseButtonNumbers} from './mouseButtonNumbers'
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the main reason behind the PR - I'd like to make "state" (like button mask and pressed keys) to be tracked by the library, it's a chore to maintain this on the consumer's side and this would match how Playwright works


const { clickCount = 1 } = options

for (let currentClick = 1; currentClick <= clickCount; currentClick++) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this loop is a fix - gonna prepare a separate PR with this in a moment and when it lands I will rebase this PR on top of that

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And there it is: #212

@@ -26,6 +27,32 @@ export interface realMouseDownOptions {
button?: keyof typeof mouseButtonNumbers;
}

export async function rawMouseDown({
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've extracted this so the "composite" realClick command can benefit from the state tracking

return {
keyCode: keyCode,
key: keyDefinition?.key ?? "",
text: keyDefinition.key.length === 1 ? keyDefinition.key : undefined,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this line is why this was needed: 5978a6e , after the change to:

-    text: keyDefinition.key.length === 1 ? keyDefinition.key : undefined,
+    text: 'text' in keyDefinition ? keyDefinition.text : undefined,

the special-casing of Enter is not needed anymore, so I've basically reverted the referenced commit

Comment on lines -79 to -85
if (key.code === "Enter") {
await fireCdpCommand("Input.dispatchKeyEvent", {
type: "char",
unmodifiedText: "\r",
text: "\r",
});
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -52,8 +43,7 @@ export async function realType(text: string, options: RealTypeOptions = {}) {
}, [] as string[]);

for (const char of chars) {
assertChar(char);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this isn't needed as getKeyCodeDefinitions throws on unknown characters anyway

@@ -52,8 +43,7 @@ export async function realType(text: string, options: RealTypeOptions = {}) {
}, [] as string[]);

for (const char of chars) {
assertChar(char);
await realPress(char, {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've refactored this to use rawRealPress (that I've introduced in this PR) since I believe that "composite" commands should use "raw" commands over the ones that are exposed publicly as Cypress commands

@@ -0,0 +1,36 @@
import { keyCodeDefinitions } from "./keyCodeDefinitions";

export type Key = keyof typeof keyCodeDefinitions;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just moved this around and tweaked slightly so both realPress and realType can use this

@@ -1,8 +1,7 @@
export const mouseButtonNumbers = {
"none": 0,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since my plan is to track the state - there should be no way to configure "none" mouse buttons for commands like realMouseDown and stuff. Unless this is needed by some kind of mouse input virtualization but I would expect such things to be pretend that some button is pressed so I'm not sure when in practice the none button could be actually used

Comment on lines +7 to +8
import { realMouseDown } from "./commands/realMouseDown";
import { realMouseUp } from "./commands/realMouseUp";
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've renamed those files to match other file names


Cypress.on('window:before:load', InternalState.clear)
Copy link
Collaborator Author

@Andarist Andarist Dec 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like a reasonable hook to clear the state. I think that this should work (although it's weird):

it('foo', () => {
  cy.visit('https://...')
  cy.get('.btn').realMouseDown()
})
it('bar', () => {
  // no visit or anything, piggybacking on the state from the previous test block
  // performing anything here should assume that the mouse button is still down
})

Base automatically changed from support-button-option to develop December 27, 2021 18:12
Copy link
Owner

@dmtrKovalenko dmtrKovalenko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not understand how this should work on the user side. Can you describe the high level API and the benefits this will bring to the users.

Thanks

@Andarist
Copy link
Collaborator Author

My main motivation is that with test scenarios like this:

cy.realKeyDown('ShiftLeft')
cy.realClick()
cy.realKeyUp('ShiftLeft')

I would expect the click to be performed with the shift key being pressed down (e.shiftKey === true). This IMHO helps people to write tests that behave more than the user because this removes the need to pass the information about the shift key "modifier" to the realClick. So it's also less error-prone for users.

@dmtrKovalenko
Copy link
Owner

dmtrKovalenko commented Jan 2, 2022

I don't really like storing a state between commands because:

  1. It's implicit. I mean developer who doesn't know that this library works this way might be confused by the fact that previous command affects the current one
  2. It's uncontrolled. In your implementation there is no clear way to turn off this behavior
  3. It's required breaking change because can affect already existing tests.

For something like this I'd like to see something like this

cy.withRealEventsContext(() => {
  cy.realKeyDown('ShiftLeft')
  cy.realClick()
  cy.realKeyUp('ShiftLeft')
})

In this case if user wants he can do it's own test function that will wrap all the internal commands inside this function. Like

const testReal = (name, fn) => it(name, () => cy.withRealEventsContext(fn))

/// then in test

testReal("it works", () => {
    cy.realKeyDown('ShiftLeft')
    cy.realClick()
    cy.realKeyUp('ShiftLeft')
})

Naming is TBD have no preference there but I'd really like to have control over the mutations that can affect future command

@Andarist
Copy link
Collaborator Author

Andarist commented Jan 2, 2022

It's implicit. I mean developer who doesn't know that this library works this way might be confused by the fact that previous command affects the current one

I agree that this is implicit but I would argue that this actually helps developers. The very same argument can be said about the opposite - I was actually confused that this sort of stuff is not handled automatically. The consumer of this package is completely not aware of the implementation details, the intricacies of the CDP protocol etc and I think that it makes perfect sense to expect a "lock" (like a key being pressed down) to stay "locked" as long as the lock is not released. This would match the behavior of a lot of existing lock-like APIs out there. This would also match the behavior of both Puppeteer and Playwright.

It's uncontrolled. In your implementation there is no clear way to turn off this behavior

It's a valid concern but also... when would you like to turn this off? At the moment I don't find any compelling scenarios when this would actually be desirable. I expect people to write tests mimicking the user. If a particular sequence of commands cannot be performed by the user then it shouldn't be allowed by the API of this library.

It's required breaking change because can affect already existing tests.

Agreed, but is it a bad thing? Some breaking changes are expected - we can batch this together with some other work. Progress requires breaking changes from time to time - we grow and learn new things about the underlying tools here and it would be a waste not to learn from that and get stuck with the older API for an undefined amount of time, right?

@Andarist
Copy link
Collaborator Author

@dmtrKovalenko any more thoughts about this one?

@dmtrKovalenko dmtrKovalenko self-requested a review January 25, 2022 18:56
@dmtrKovalenko
Copy link
Owner

dmtrKovalenko commented Feb 11, 2022

Again sorry for long answering but:

I agree that this is implicit but I would argue that this actually helps developers. The very same argument can be said about the opposite - I was actually confused that this sort of stuff is not handled automatically. The consumer of this package is completely not aware of the implementation details, the intricacies of the CDP protocol etc and I think that it makes perfect sense to expect a "lock" (like a key being pressed down) to stay "locked" as long as the lock is not released.

The same works opposite. I personally will expect all the commands to be completely standalone and do not imply on the other commands

It's a valid concern but also... when would you like to turn this off

I would like to avoid this behavior as much as possible and execute one command at a time. Because I know too much about how testing drivers works inside :D. But in fact yes it can confuse somebody when you execute a command and 3 commands after it affects something else.

Make sure that user can do the keydown in a shared command which is out of context

Some breaking changes are expected

I don't think this is a valid reason to make a breaking change for this type of feature

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants