Skip to content

Commit e871ba7

Browse files
Stop reorder-scaled-parent Cypress from racing the drag frame loop.
PanSession applies pointermove on the next animation frame, so one-shot .then() checks after a short wait flake on slow CI. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 0f1c119 commit e871ba7

2 files changed

Lines changed: 68 additions & 82 deletions

File tree

dev/react/src/tests/reorder-scaled-parent.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ const Item = ({ item }: { item: number }) => {
3737
value={item}
3838
id={`item-${item}`}
3939
data-testid={`item-${item}`}
40+
dragMomentum={false}
41+
transition={{ duration: 0 }}
4042
style={{
4143
y,
4244
height: 50,

packages/framer-motion/cypress/integration/reorder-scaled-parent.ts

Lines changed: 66 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -11,112 +11,96 @@
1111
* workaround is `correctParentTransform(ref)` via `MotionConfig
1212
* transformPagePoint`. These tests pin that workaround so future drag /
1313
* projection refactors can't silently regress it.
14+
*
15+
* Pointer coords are element-relative (same pattern as `drag-scaled-parent.ts`).
16+
* PanSession applies pointermove on the next animation frame, so end-state
17+
* checks use `.should()` — a single `.then()` after a fixed wait races RAF
18+
* on slow CI.
1419
*/
15-
function domOrder(): Cypress.Chainable<string[]> {
16-
return cy.get("#reorder-group [data-testid]").then(($items) =>
17-
Cypress._.map($items, (el) => el.getAttribute("data-testid") as string)
20+
function itemOrder($items: any): string[] {
21+
return Cypress._.map(
22+
$items,
23+
(el) => el.getAttribute("data-testid") as string
1824
)
1925
}
2026

2127
describe("Reorder inside a scaled parent (#2449 / #2750)", () => {
2228
it("dragged item tracks the cursor with correctParentTransform (scale 0.5)", () => {
2329
cy.visit("?test=reorder-scaled-parent&corrected=true&scale=0.5")
24-
.wait(300)
25-
.get("[data-testid='item-0']")
26-
.then(($el: any) => {
27-
const start = $el[0].getBoundingClientRect()
28-
const startMidX = start.left + start.width / 2
29-
const startMidY = start.top + start.height / 2
30-
31-
cy.wrap($el)
32-
.trigger("pointerdown", startMidX, startMidY, {
33-
force: true,
34-
})
35-
.wait(50)
36-
// Move past the drag threshold.
37-
.trigger("pointermove", startMidX, startMidY + 5, {
38-
force: true,
39-
})
40-
.wait(50)
41-
// Move the pointer 80px down the screen.
42-
.trigger("pointermove", startMidX, startMidY + 80, {
43-
force: true,
44-
})
45-
.wait(80)
46-
.then(([item]: any) => {
47-
const moved = item.getBoundingClientRect()
48-
const movedMidY = moved.top + moved.height / 2
49-
const screenDelta = movedMidY - startMidY
50-
51-
// The element must follow the cursor (~80px on screen),
52-
// not 80 / scale (160px) as it would without the
53-
// transformPagePoint correction.
54-
expect(screenDelta).to.be.greaterThan(60)
55-
expect(screenDelta).to.be.lessThan(100)
56-
})
57-
.trigger("pointerup", { force: true })
58-
})
59-
})
60-
61-
it("reorders correctly and settles aligned with correctParentTransform", () => {
62-
cy.visit("?test=reorder-scaled-parent&corrected=true&scale=0.5")
63-
.wait(300)
64-
65-
domOrder().should("deep.equal", [
66-
"item-0",
67-
"item-1",
68-
"item-2",
69-
"item-3",
70-
])
30+
cy.get("[data-testid='item-0']").should("be.visible")
7131

7232
cy.get("[data-testid='item-0']").then(($el: any) => {
7333
const start = $el[0].getBoundingClientRect()
74-
const startMidX = start.left + start.width / 2
7534
const startMidY = start.top + start.height / 2
7635

77-
let chain = cy
78-
.wrap($el)
79-
.trigger("pointerdown", startMidX, startMidY, { force: true })
36+
cy.wrap($el)
37+
.trigger("pointerdown", 20, 10, { force: true })
8038
.wait(50)
81-
// Move past the drag threshold first.
82-
.trigger("pointermove", startMidX, startMidY + 6, {
83-
force: true,
84-
})
39+
// Move past the drag threshold.
40+
.trigger("pointermove", 20, 16, { force: true })
8541
.wait(50)
42+
// Move the pointer 80px down the screen.
43+
.trigger("pointermove", 20, 90, { force: true })
8644

87-
// Steady drag downward. With scale 0.5 the offset is corrected
88-
// into local space, so the on-screen distance must be ~2x the
89-
// local row pitch to cross item-1's centre. Incremental moves
90-
// keep a non-zero velocity so checkReorder engages.
91-
for (let i = 1; i <= 6; i++) {
92-
chain = chain
93-
.trigger("pointermove", startMidX, startMidY + i * 14, {
94-
force: true,
95-
})
96-
.wait(60)
97-
}
45+
// Drag tracking is 1:1, not a tween: retry until the frame loop
46+
// applies the pointermove. A wrong scale (e.g. 40px uncorrected)
47+
// stays wrong and still fails.
48+
cy.wrap($el).should(($item: any) => {
49+
const moved = $item[0].getBoundingClientRect()
50+
const screenDelta = moved.top + moved.height / 2 - startMidY
9851

99-
chain.trigger("pointerup", { force: true }).wait(700)
52+
// The element must follow the cursor (~80px on screen),
53+
// not 80 / scale (160px) or 80 * scale (40px) as it would
54+
// without the transformPagePoint correction.
55+
expect(screenDelta).to.be.greaterThan(60)
56+
expect(screenDelta).to.be.lessThan(100)
57+
})
58+
59+
cy.wrap($el).trigger("pointerup", { force: true })
60+
})
61+
})
62+
63+
it("reorders correctly and settles aligned with correctParentTransform", () => {
64+
cy.visit("?test=reorder-scaled-parent&corrected=true&scale=0.5")
65+
cy.get("[data-testid='item-0']").should("be.visible")
66+
67+
cy.get("#reorder-group [data-testid]").should(($items) => {
68+
expect(itemOrder($items)).to.deep.equal([
69+
"item-0",
70+
"item-1",
71+
"item-2",
72+
"item-3",
73+
])
10074
})
10175

102-
// item-0 should have moved down past item-1 (a swap occurred) ...
103-
domOrder().then((order) => {
76+
// Element-relative moves. Scale 0.5 → ~40px screen is ~80px local,
77+
// enough to cross item-1's centre (needs >35px local) without
78+
// racing through the whole list.
79+
cy.get("[data-testid='item-0']")
80+
.trigger("pointerdown", 20, 10, { force: true })
81+
.wait(50)
82+
.trigger("pointermove", 20, 16, { force: true })
83+
.wait(50)
84+
.trigger("pointermove", 20, 35, { force: true })
85+
.wait(50)
86+
.trigger("pointermove", 20, 50, { force: true })
87+
.wait(50)
88+
.trigger("pointerup", { force: true })
89+
90+
cy.get("#reorder-group [data-testid]").should(($items) => {
91+
const order = itemOrder($items)
10492
expect(order.indexOf("item-0")).to.be.greaterThan(
10593
order.indexOf("item-1"),
10694
`expected item-0 to sit below item-1, got ${order.join(",")}`
10795
)
10896
})
10997

110-
// ... and the released item should settle flush in the list, i.e. no
111-
// stranded drag transform (translateY ≈ 0 in local space).
112-
cy.get("[data-testid='item-0']").then(($el: any) => {
113-
const transform = getComputedStyle($el[0]).transform
114-
if (transform && transform !== "none") {
115-
const parts = transform.match(/matrix\(([^)]+)\)/)
116-
if (parts) {
117-
const translateY = parseFloat(parts[1].split(",")[5])
118-
expect(Math.abs(translateY)).to.be.lessThan(10)
119-
}
98+
// List sits flush: 10px local gap × 0.5 scale = 5px screen, no
99+
// leftover drag/layout transform. Retry until springs finish.
100+
cy.get("#reorder-group [data-testid]").should(($items) => {
101+
const boxes = [...$items].map((el) => el.getBoundingClientRect())
102+
for (let i = 1; i < boxes.length; i++) {
103+
expect(boxes[i].top).to.be.closeTo(boxes[i - 1].bottom + 5, 4)
120104
}
121105
})
122106
})

0 commit comments

Comments
 (0)