|
1 | | -import { render, screen } from "@testing-library/react-native"; |
| 1 | +import { act, render, screen } from "@testing-library/react-native"; |
2 | 2 | import { VariableContextProvider } from "react-native-css"; |
3 | 3 | import { View } from "react-native-css/components/View"; |
4 | 4 | import { registerCSS, testID } from "react-native-css/jest"; |
5 | | -import { nonInheritedVariables } from "react-native-css/native-internal"; |
| 5 | +import { |
| 6 | + nonInheritedVariables, |
| 7 | + registeredInitialValues, |
| 8 | + resetVariableRegistries, |
| 9 | +} from "react-native-css/native-internal"; |
6 | 10 |
|
7 | 11 | const parentTestID = "parent"; |
8 | 12 |
|
@@ -342,6 +346,102 @@ test("re-registering keeps the registry object identity", () => { |
342 | 346 | expect(globalThis.__react_native_css_non_inherited_variables).toBe(before); |
343 | 347 | }); |
344 | 348 |
|
| 349 | +/* ------------------------------------------------------------------ * |
| 350 | + * Lifecycle — a reload retracts a registered initial value |
| 351 | + * ------------------------------------------------------------------ */ |
| 352 | + |
| 353 | +// Composed into arithmetic ON THE DECLARING ELEMENT, which is how Tailwind reads |
| 354 | +// `--tw-ring-offset-width`: `calc(2px + var(--tw-ring-offset-width))`. A registration that |
| 355 | +// outlives the rule declaring it does not hand a descendant something it should not have |
| 356 | +// inherited, it corrupts a length an element computes for itself |
| 357 | +const initialValueRegistration = ` |
| 358 | + @property --my-var { |
| 359 | + syntax: "<length>"; |
| 360 | + inherits: false; |
| 361 | + initial-value: 3px; |
| 362 | + } |
| 363 | +`; |
| 364 | + |
| 365 | +const initialValueConsumer = ` |
| 366 | + .probe { width: calc(2px + var(--my-var)); } |
| 367 | + .a { --my-var: 10px; } |
| 368 | + .b { --my-var: 20px; } |
| 369 | +`; |
| 370 | + |
| 371 | +test("a sheet registering nothing leaves the consumer no width", () => { |
| 372 | + // The control for the two tests below. `.probe` reads a property no rule it matches |
| 373 | + // declares and no @property registers, so the whole declaration drops |
| 374 | + registerCSS(initialValueConsumer); |
| 375 | + |
| 376 | + render(<View testID={testID} className="probe" />); |
| 377 | + |
| 378 | + expect(screen.getByTestId(testID).props.style).toStrictEqual({}); |
| 379 | +}); |
| 380 | + |
| 381 | +test("deleting an @property rule retracts its initial value", () => { |
| 382 | + registerCSS(` |
| 383 | + ${initialValueRegistration} |
| 384 | + ${initialValueConsumer} |
| 385 | + `); |
| 386 | + expect(registeredInitialValues("my-var").get()).toBe(3); |
| 387 | + |
| 388 | + registerCSS(initialValueConsumer); |
| 389 | + |
| 390 | + expect(registeredInitialValues("my-var").get()).toBeUndefined(); |
| 391 | +}); |
| 392 | + |
| 393 | +test("a mounted element drops a retracted initial value", () => { |
| 394 | + // Deleting an @property rule un-registered only half of it: the name left |
| 395 | + // `nonInheritedVariables` and the initial value stayed, so the two halves of one |
| 396 | + // registration disagreed and the element kept painting a width the sheet no longer |
| 397 | + // declares anywhere |
| 398 | + registerCSS(` |
| 399 | + ${initialValueRegistration} |
| 400 | + ${initialValueConsumer} |
| 401 | + `); |
| 402 | + |
| 403 | + render(<View testID={testID} className="probe" />); |
| 404 | + expect(screen.getByTestId(testID).props.style).toStrictEqual({ width: 5 }); |
| 405 | + |
| 406 | + act(() => { |
| 407 | + registerCSS(initialValueConsumer); |
| 408 | + }); |
| 409 | + |
| 410 | + // Exactly what the same sheet paints when it is the first one loaded, two tests above |
| 411 | + expect(screen.getByTestId(testID).props.style).toStrictEqual({}); |
| 412 | +}); |
| 413 | + |
| 414 | +test("retracting an initial value keeps the observable its readers hold", () => { |
| 415 | + // This is why the retraction is `.set(undefined)` and not `.clear()`. Clearing the family |
| 416 | + // drops the map entry without notifying anyone, so a mounted reader keeps the deleted |
| 417 | + // value AND the next registration of the same name lands on an observable it never |
| 418 | + // subscribed to — the reader is then stranded for the rest of the session |
| 419 | + registerCSS(` |
| 420 | + ${initialValueRegistration} |
| 421 | + ${initialValueConsumer} |
| 422 | + `); |
| 423 | + const before = registeredInitialValues("my-var"); |
| 424 | + |
| 425 | + registerCSS(initialValueConsumer); |
| 426 | + |
| 427 | + expect(registeredInitialValues("my-var")).toBe(before); |
| 428 | +}); |
| 429 | + |
| 430 | +test("resetVariableRegistries retracts a registered initial value", () => { |
| 431 | + // The jest preset's beforeEach is all that stands between one test's @property |
| 432 | + // registration and the next test's. A plain clear() is right HERE and wrong in inject(): |
| 433 | + // testing-library unmounts between tests, so this retraction has no reader to strand |
| 434 | + registerCSS(` |
| 435 | + ${initialValueRegistration} |
| 436 | + ${initialValueConsumer} |
| 437 | + `); |
| 438 | + expect(registeredInitialValues("my-var").get()).toBe(3); |
| 439 | + |
| 440 | + resetVariableRegistries(); |
| 441 | + |
| 442 | + expect(registeredInitialValues("my-var").get()).toBeUndefined(); |
| 443 | +}); |
| 444 | + |
345 | 445 | /* ------------------------------------------------------------------ * |
346 | 446 | * Tailwind v4 ring composition, end to end |
347 | 447 | * ------------------------------------------------------------------ */ |
|
0 commit comments