summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/html/semantics/popovers/popover-target-action-hover.tentative.html
blob: b03ec78ebf12e49775bf0636c78a803d97a1c19d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
<!DOCTYPE html>
<meta charset="utf-8" />
<title>The popovertargetaction=hover behavior</title>
<link rel="author" href="mailto:masonf@chromium.org">
<link rel=help href="https://open-ui.org/components/popover.research.explainer">
<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-actions.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="resources/popover-utils.js"></script>

<body>
<style>
.unrelated {top:0;}
.invoker {top:100px; width:fit-content; height:fit-content;}
[popover] {top: 200px;}
.offset-child {top:300px; left:300px;}
</style>

<script>
const popoverShowDelay = 100; // The CSS delay setting.
const hoverWaitTime = 200; // How long to wait to cover the delay for sure.
async function makePopoverAndInvoker(test, popoverType, invokerType, delayMs) {
  delayMs = delayMs || popoverShowDelay;
  const popover = Object.assign(document.createElement('div'),{popover: popoverType});
  document.body.appendChild(popover);
  popover.textContent = 'Popover';
  // Set popover-show-delay on the popover to 0 - it should be ignored.
  popover.setAttribute('style',`popover-show-delay: 0; popover-hide-delay: 1000s;`);
  let invoker = document.createElement('button');
  invoker.setAttribute('class','invoker');
  invoker.popoverTargetElement = popover;
  invoker.popoverTargetAction = "hover";
  // Set popover-hide-delay on the invoker to 0 - it should be ignored.
  invoker.setAttribute('style',`popover-show-delay: ${delayMs}ms; popover-hide-delay: 0;`);
  document.body.appendChild(invoker);
  const actualHoverDelay = Number(getComputedStyle(invoker)['popoverShowDelay'].slice(0,-1))*1000;
  assert_equals(actualHoverDelay,delayMs,'popover-show-delay is incorrect');
  const originalInvoker = invoker;
  const reassignPopoverFn = (p) => {originalInvoker.popoverTargetElement = p};
  switch (invokerType) {
    case 'plain':
      // Invoker is just a button.
      invoker.textContent = 'Invoker';
      break;
    case 'nested':
      // Invoker is just a button containing a div.
      const child1 = invoker.appendChild(document.createElement('div'));
      child1.textContent = 'Invoker';
      break;
    case 'nested-offset':
      // Invoker is a child of the invoking button, and is not contained within
      // the bounds of the popovertarget element.
      invoker.textContent = 'Invoker';
      // Reassign invoker to the child:
      invoker = invoker.appendChild(document.createElement('div'));
      invoker.textContent = 'Invoker child';
      invoker.setAttribute('class','offset-child');
      break;
    case 'none':
      // No invoker.
      invoker.remove();
      break;
    default:
      assert_unreached(`Invalid invokerType ${invokerType}`);
  }
  const unrelated = document.createElement('div');
  document.body.appendChild(unrelated);
  unrelated.textContent = 'Unrelated';
  unrelated.setAttribute('class','unrelated');
  test.add_cleanup(async () => {
    popover.remove();
    invoker.remove();
    originalInvoker.remove();
    unrelated.remove();
    await waitForRender();
  });
  await mouseOver(unrelated); // Start by mousing over the unrelated element
  await waitForRender();
  return {popover,invoker,reassignPopoverFn};
}

// NOTE about testing methodology:
// This test checks whether popovers are triggered *after* the appropriate hover
// delay. The delay used for testing is kept low, to avoid this test taking too
// long, but that means that sometimes on a slow bot/client, the hover delay can
// elapse before we are able to check the popover status. And that can make this
// test flaky. To avoid that, the msSinceMouseOver() function is used to check
// that not-too-much time has passed, and if it has, the test is simply skipped.

["auto","hint","manual"].forEach(type => {
  ["plain","nested","nested-offset"].forEach(invokerType => {
    promise_test(async (t) => {
      const {popover,invoker} = await makePopoverAndInvoker(t,type,invokerType);
      assert_false(popover.matches(':popover-open'));
      await mouseOver(invoker);
      let showing = popover.matches(':popover-open');
      // See NOTE above.
      if (msSinceMouseOver() < popoverShowDelay)
        assert_false(showing,'popover should not show immediately');
      await waitForHoverTime(hoverWaitTime);
      assert_true(msSinceMouseOver() >= hoverWaitTime,'waitForHoverTime should wait the specified time');
      assert_true(popover.matches(':popover-open'),'popover should show after delay');
      assert_true(hoverWaitTime > popoverShowDelay,'popoverShowDelay is the CSS setting, hoverWaitTime should be longer than that');
      popover.hidePopover(); // Cleanup
    },`popovertargetaction=hover shows a popover with popover=${type}, invokerType=${invokerType}`);

    promise_test(async (t) => {
      const {popover,invoker} = await makePopoverAndInvoker(t,type,invokerType);
      assert_false(popover.matches(':popover-open'));
      invoker.click(); // Click the invoker
      assert_true(popover.matches(':popover-open'),'Clicking the invoker should show the popover, even when popovertargetaction=hover');
      popover.hidePopover(); // Cleanup
    },`popovertargetaction=hover should also allow click activation, for popover=${type}, invokerType=${invokerType}`);

    promise_test(async (t) => {
      const longerHoverDelay = hoverWaitTime*2;
      const {popover,invoker} = await makePopoverAndInvoker(t,type,invokerType,longerHoverDelay);
      await mouseOver(invoker);
      let showing = popover.matches(':popover-open');
      // See NOTE above.
      if (msSinceMouseOver() >= longerHoverDelay)
        return; // The WPT runner was too slow.
      assert_false(showing,'popover should not show immediately');
      await waitForHoverTime(hoverWaitTime);
      showing = popover.matches(':popover-open');
      if (msSinceMouseOver() >= longerHoverDelay)
        return; // The WPT runner was too slow.
      assert_false(showing,'popover should not show after not long enough of a delay');
    },`popovertargetaction=hover popover-show-delay is respected (popover=${type}, invokerType=${invokerType})`);

    promise_test(async (t) => {
      const {popover,invoker} = await makePopoverAndInvoker(t,type,invokerType);
      popover.showPopover();
      assert_true(popover.matches(':popover-open'));
      await mouseOver(invoker);
      assert_true(popover.matches(':popover-open'),'popover should stay showing on mouseover');
      await waitForHoverTime(hoverWaitTime);
      assert_true(popover.matches(':popover-open'),'popover should stay showing after delay');
      popover.hidePopover(); // Cleanup
    },`popovertargetaction=hover does nothing when popover is already showing (popover=${type}, invokerType=${invokerType})`);

    promise_test(async (t) => {
      const {popover,invoker} = await makePopoverAndInvoker(t,type,invokerType);
      await mouseOver(invoker);
      let showing = popover.matches(':popover-open');
      popover.remove();
      // See NOTE above.
      if (msSinceMouseOver() >= popoverShowDelay)
        return; // The WPT runner was too slow.
      assert_false(showing,'popover should not show immediately');
      await waitForHoverTime(hoverWaitTime);
      assert_false(popover.matches(':popover-open'),'popover should not show even after a delay');
      // Now put it back in the document and make sure it doesn't trigger.
      document.body.appendChild(popover);
      await waitForHoverTime(hoverWaitTime);
      assert_false(popover.matches(':popover-open'),'popover should not show even when returned to the document');
    },`popovertargetaction=hover does nothing when popover is moved out of the document (popover=${type}, invokerType=${invokerType})`);

    promise_test(async (t) => {
      const {popover,invoker,reassignPopoverFn} = await makePopoverAndInvoker(t,type,invokerType);
      const popover2 = Object.assign(document.createElement('div'),{popover: type});
      document.body.appendChild(popover2);
      t.add_cleanup(() => popover2.remove());
      await mouseOver(invoker);
      let eitherShowing = popover.matches(':popover-open') || popover2.matches(':popover-open');
      reassignPopoverFn(popover2);
      // See NOTE above.
      if (msSinceMouseOver() >= popoverShowDelay)
        return; // The WPT runner was too slow.
      assert_false(eitherShowing,'popover should not show immediately');
      await waitForHoverTime(hoverWaitTime);
      assert_false(popover.matches(':popover-open'),'popover #1 should not show since popovertarget was reassigned');
      assert_false(popover2.matches(':popover-open'),'popover #2 should not show since popovertarget was reassigned');
    },`popovertargetaction=hover does nothing when target changes (popover=${type}, invokerType=${invokerType})`);
  });
});
</script>