|
| 1 | +import {getConfig} from '@testing-library/dom' |
| 2 | + |
1 | 3 | const dispose: typeof Symbol.dispose =
|
2 | 4 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
3 | 5 | Symbol.dispose ?? Symbol.for('nodejs.dispose')
|
4 | 6 |
|
| 7 | +export interface DisableActEnvironmentOptions { |
| 8 | + /** |
| 9 | + * If `true`, all modifications of values set by `disableActEnvironment` |
| 10 | + * will be prevented until `cleanup` is called. |
| 11 | + * |
| 12 | + * @default true |
| 13 | + */ |
| 14 | + preventModification?: boolean |
| 15 | + |
| 16 | + /** |
| 17 | + * If `true`, will change the configuration of the testing library to |
| 18 | + * prevent auto-wrapping e.g. `userEvent` calls in `act`. |
| 19 | + * |
| 20 | + * @default true |
| 21 | + */ |
| 22 | + adjustTestingLibConfig?: boolean |
| 23 | +} |
| 24 | + |
5 | 25 | /**
|
6 | 26 | * Helper to temporarily disable a React 18+ act environment.
|
7 | 27 | *
|
| 28 | + * By default, this also adjusts the configuration of @testing-library/dom |
| 29 | + * to prevent auto-wrapping of user events in `act`, as well as preventing |
| 30 | + * all modifications of values set by this method until `cleanup` is called |
| 31 | + * or the returned `Disposable` is disposed of. |
| 32 | + * |
| 33 | + * Both of these behaviors can be disabled with the option, of the defaults |
| 34 | + * can be changed for all calls to this method by modifying |
| 35 | + * `disableActEnvironment.defaultOptions`. |
| 36 | + * |
8 | 37 | * This returns a disposable and can be used in combination with `using` to
|
9 | 38 | * automatically restore the state from before this method call after your test.
|
10 | 39 | *
|
@@ -35,16 +64,95 @@ const dispose: typeof Symbol.dispose =
|
35 | 64 | * For more context on what `act` is and why you shouldn't use it in renderStream tests,
|
36 | 65 | * https://github.com/reactwg/react-18/discussions/102 is probably the best resource we have.
|
37 | 66 | */
|
38 |
| -export function disableActEnvironment(): {cleanup: () => void} & Disposable { |
39 |
| - const anyThis = globalThis as any as {IS_REACT_ACT_ENVIRONMENT?: boolean} |
40 |
| - const prevActEnv = anyThis.IS_REACT_ACT_ENVIRONMENT |
41 |
| - anyThis.IS_REACT_ACT_ENVIRONMENT = false |
| 67 | +export function disableActEnvironment({ |
| 68 | + preventModification = disableActEnvironment.defaultOptions |
| 69 | + .preventModification, |
| 70 | + adjustTestingLibConfig = disableActEnvironment.defaultOptions |
| 71 | + .adjustTestingLibConfig, |
| 72 | +}: DisableActEnvironmentOptions = {}): {cleanup: () => void} & Disposable { |
| 73 | + const typedGlobal = globalThis as any as {IS_REACT_ACT_ENVIRONMENT?: boolean} |
| 74 | + const cleanupFns: Array<() => void> = [] |
| 75 | + |
| 76 | + // core functionality |
| 77 | + { |
| 78 | + const previous = typedGlobal.IS_REACT_ACT_ENVIRONMENT |
| 79 | + cleanupFns.push(() => { |
| 80 | + Object.defineProperty(typedGlobal, 'IS_REACT_ACT_ENVIRONMENT', { |
| 81 | + value: previous, |
| 82 | + }) |
| 83 | + }) |
| 84 | + Object.defineProperty( |
| 85 | + typedGlobal, |
| 86 | + 'IS_REACT_ACT_ENVIRONMENT', |
| 87 | + getNewPropertyDescriptor(false, preventModification), |
| 88 | + ) |
| 89 | + } |
| 90 | + |
| 91 | + if (adjustTestingLibConfig) { |
| 92 | + const config = getConfig() |
| 93 | + // eslint-disable-next-line @typescript-eslint/unbound-method |
| 94 | + const {asyncWrapper, eventWrapper} = config |
| 95 | + cleanupFns.push(() => { |
| 96 | + Object.defineProperty(config, 'asyncWrapper', {value: asyncWrapper}) |
| 97 | + Object.defineProperty(config, 'eventWrapper', {value: eventWrapper}) |
| 98 | + }) |
| 99 | + |
| 100 | + Object.defineProperty( |
| 101 | + config, |
| 102 | + 'asyncWrapper', |
| 103 | + getNewPropertyDescriptor<typeof asyncWrapper>( |
| 104 | + fn => fn(), |
| 105 | + preventModification, |
| 106 | + ), |
| 107 | + ) |
| 108 | + Object.defineProperty( |
| 109 | + config, |
| 110 | + 'eventWrapper', |
| 111 | + getNewPropertyDescriptor<typeof eventWrapper>( |
| 112 | + fn => fn(), |
| 113 | + preventModification, |
| 114 | + ), |
| 115 | + ) |
| 116 | + } |
42 | 117 |
|
43 | 118 | function cleanup() {
|
44 |
| - anyThis.IS_REACT_ACT_ENVIRONMENT = prevActEnv |
| 119 | + while (cleanupFns.length > 0) { |
| 120 | + cleanupFns.pop()!() |
| 121 | + } |
45 | 122 | }
|
46 | 123 | return {
|
47 | 124 | cleanup,
|
48 | 125 | [dispose]: cleanup,
|
49 | 126 | }
|
50 | 127 | }
|
| 128 | + |
| 129 | +/** |
| 130 | + * Default options for `disableActEnvironment`. |
| 131 | + * |
| 132 | + * This can be modified to change the default options for all calls to `disableActEnvironment`. |
| 133 | + */ |
| 134 | +disableActEnvironment.defaultOptions = { |
| 135 | + preventModification: true, |
| 136 | + adjustTestingLibConfig: true, |
| 137 | +} satisfies Required<DisableActEnvironmentOptions> as Required<DisableActEnvironmentOptions> |
| 138 | + |
| 139 | +function getNewPropertyDescriptor<T>( |
| 140 | + value: T, |
| 141 | + preventModification: boolean, |
| 142 | +): PropertyDescriptor { |
| 143 | + return preventModification |
| 144 | + ? { |
| 145 | + configurable: true, |
| 146 | + enumerable: true, |
| 147 | + get() { |
| 148 | + return value |
| 149 | + }, |
| 150 | + set() {}, |
| 151 | + } |
| 152 | + : { |
| 153 | + configurable: true, |
| 154 | + enumerable: true, |
| 155 | + writable: true, |
| 156 | + value, |
| 157 | + } |
| 158 | +} |
0 commit comments