diff --git a/core/src/components/refresher/refresher.tsx b/core/src/components/refresher/refresher.tsx
index e62342dec9..56b85f7470 100644
--- a/core/src/components/refresher/refresher.tsx
+++ b/core/src/components/refresher/refresher.tsx
@@ -4,7 +4,12 @@ import { Component, Element, Event, Host, Method, Prop, State, Watch, h, readTas
import { getIonMode } from '../../global/ionic-global';
import type { Animation, Gesture, GestureDetail, RefresherEventDetail } from '../../interface';
import { getTimeGivenProgression } from '../../utils/animation/cubic-bezier';
-import { findClosestIonContent, getScrollElement, printIonContentErrorMsg } from '../../utils/content';
+import {
+ getScrollElement,
+ ION_CONTENT_CLASS_SELECTOR,
+ ION_CONTENT_ELEMENT_SELECTOR,
+ printIonContentErrorMsg,
+} from '../../utils/content';
import { clamp, getElementRoot, raf, transitionEndAsync } from '../../utils/helpers';
import { hapticImpact } from '../../utils/native/haptic';
@@ -436,13 +441,21 @@ export class Refresher implements ComponentInterface {
return;
}
- const contentEl = findClosestIonContent(this.el);
+ const contentEl = this.el.closest(ION_CONTENT_ELEMENT_SELECTOR);
if (!contentEl) {
printIonContentErrorMsg(this.el);
return;
}
- this.scrollEl = await getScrollElement(contentEl);
+ const customScrollTarget = contentEl.querySelector(ION_CONTENT_CLASS_SELECTOR);
+
+ /**
+ * Query the custom scroll target (if available), first. In refresher implementations,
+ * the ion-refresher element will always be a direct child of ion-content (slot="fixed"). By
+ * querying the custom scroll target first and falling back to the ion-content element,
+ * the correct scroll element will be returned by the implementation.
+ */
+ this.scrollEl = await getScrollElement(customScrollTarget ?? contentEl);
/**
* Query the host `ion-content` directly (if it is available), to use its
@@ -452,9 +465,7 @@ export class Refresher implements ComponentInterface {
* This makes it so that implementers do not need to re-create the background content
* element and styles.
*/
- const backgroundContentHost = this.el.closest('ion-content') ?? contentEl;
-
- this.backgroundContentEl = getElementRoot(backgroundContentHost).querySelector(
+ this.backgroundContentEl = getElementRoot(contentEl ?? customScrollTarget).querySelector(
'#background-content'
) as HTMLElement;
diff --git a/core/src/components/refresher/test/basic/e2e.ts b/core/src/components/refresher/test/basic/e2e.ts
deleted file mode 100644
index 86d9d9117f..0000000000
--- a/core/src/components/refresher/test/basic/e2e.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import type { E2EPage } from '@stencil/core/testing';
-import { newE2EPage } from '@stencil/core/testing';
-
-import { pullToRefresh } from '../test.utils';
-
-describe('refresher: basic', () => {
- let page: E2EPage;
-
- beforeEach(async () => {
- page = await newE2EPage({
- url: '/src/components/refresher/test/basic?ionic:_testing=true',
- });
- });
-
- it('should match existing visual screenshots', async () => {
- const compare = await page.compareScreenshot();
- expect(compare).toMatchScreenshot();
- });
-
- describe('legacy refresher', () => {
- it('should load more items when performing a pull-to-refresh', async () => {
- const initialItems = await page.findAll('ion-item');
- expect(initialItems.length).toBe(30);
-
- await pullToRefresh(page);
-
- const items = await page.findAll('ion-item');
- expect(items.length).toBe(60);
- });
- });
-
- describe('native refresher', () => {
- it('should load more items when performing a pull-to-refresh', async () => {
- const refresherContent = await page.$('ion-refresher-content');
- refresherContent.evaluate((el: any) => {
- // Resets the pullingIcon to enable the native refresher
- el.pullingIcon = undefined;
- });
-
- await page.waitForChanges();
-
- const initialItems = await page.findAll('ion-item');
- expect(initialItems.length).toBe(30);
-
- await pullToRefresh(page);
-
- const items = await page.findAll('ion-item');
- expect(items.length).toBe(60);
- });
- });
-});
diff --git a/core/src/components/refresher/test/basic/index.html b/core/src/components/refresher/test/basic/index.html
index 51a6e3000f..60982df867 100644
--- a/core/src/components/refresher/test/basic/index.html
+++ b/core/src/components/refresher/test/basic/index.html
@@ -53,7 +53,7 @@
refresher.complete();
render();
// Custom event consumed by e2e tests
- document.dispatchEvent(new CustomEvent('ionRefreshComplete'));
+ window.dispatchEvent(new CustomEvent('ionRefreshComplete'));
});
function render() {
diff --git a/core/src/components/refresher/test/basic/refresher.e2e.ts b/core/src/components/refresher/test/basic/refresher.e2e.ts
new file mode 100644
index 0000000000..c67fca972a
--- /dev/null
+++ b/core/src/components/refresher/test/basic/refresher.e2e.ts
@@ -0,0 +1,41 @@
+import { expect } from '@playwright/test';
+import { test } from '@utils/test/playwright';
+
+import { pullToRefresh } from '../test.utils';
+
+test.describe('refresher: basic', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/src/components/refresher/test/basic');
+ });
+
+ test.describe('legacy refresher', () => {
+ test('should load more items when performing a pull-to-refresh', async ({ page }) => {
+ const items = page.locator('ion-item');
+
+ expect(await items.count()).toBe(30);
+
+ await pullToRefresh(page);
+
+ expect(await items.count()).toBe(60);
+ });
+ });
+
+ test.describe('native refresher', () => {
+ test('should load more items when performing a pull-to-refresh', async ({ page }) => {
+ const refresherContent = page.locator('ion-refresher-content');
+ refresherContent.evaluateHandle((el: any) => {
+ // Resets the pullingIcon to enable the native refresher
+ el.pullingIcon = undefined;
+ });
+
+ await page.waitForChanges();
+
+ const items = page.locator('ion-item');
+ expect(await items.count()).toBe(30);
+
+ await pullToRefresh(page);
+
+ expect(await items.count()).toBe(60);
+ });
+ });
+});
diff --git a/core/src/components/refresher/test/scroll-target/e2e.ts b/core/src/components/refresher/test/scroll-target/e2e.ts
deleted file mode 100644
index 1920e75924..0000000000
--- a/core/src/components/refresher/test/scroll-target/e2e.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import type { E2EPage } from '@stencil/core/testing';
-import { newE2EPage } from '@stencil/core/testing';
-
-import { pullToRefresh } from '../test.utils';
-
-// TODO(FW-1134) Re-write these tests so that they test correct functionality.
-describe.skip('refresher: custom scroll target', () => {
- let page: E2EPage;
-
- beforeEach(async () => {
- page = await newE2EPage({
- url: '/src/components/refresher/test/scroll-target?ionic:_testing=true',
- });
- });
-
- describe('legacy refresher', () => {
- it('should load more items when performing a pull-to-refresh', async () => {
- const initialItems = await page.findAll('ion-item');
- expect(initialItems.length).toBe(30);
-
- await pullToRefresh(page);
-
- const items = await page.findAll('ion-item');
- expect(items.length).toBe(60);
- });
- });
-
- describe('native refresher', () => {
- it('should load more items when performing a pull-to-refresh', async () => {
- const refresherContent = await page.$('ion-refresher-content');
- refresherContent.evaluate((el: any) => {
- // Resets the pullingIcon to enable the native refresher
- el.pullingIcon = undefined;
- });
-
- await page.waitForChanges();
-
- const initialItems = await page.findAll('ion-item');
- expect(initialItems.length).toBe(30);
-
- await pullToRefresh(page);
-
- const items = await page.findAll('ion-item');
- expect(items.length).toBe(60);
- });
- });
-});
diff --git a/core/src/components/refresher/test/scroll-target/index.html b/core/src/components/refresher/test/scroll-target/index.html
index fe38d97e99..caa467fa3b 100644
--- a/core/src/components/refresher/test/scroll-target/index.html
+++ b/core/src/components/refresher/test/scroll-target/index.html
@@ -44,8 +44,8 @@