diff --git a/detox/android/detox/src/full/java/com/wix/detox/espresso/DetoxAction.java b/detox/android/detox/src/full/java/com/wix/detox/espresso/DetoxAction.java index 8474e3526d..8ffa60d061 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/espresso/DetoxAction.java +++ b/detox/android/detox/src/full/java/com/wix/detox/espresso/DetoxAction.java @@ -79,9 +79,14 @@ public float[] calculateCoordinates(View view) { * Scrolls to the edge of the given scrollable view. * * @param edge Direction to scroll (see {@link MotionDir}) + * @param startOffsetPercentX Percentage denoting where the scroll should start from on the X-axis, with respect to the scrollable view. + * @param startOffsetPercentY Percentage denoting where the scroll should start from on the Y-axis, with respect to the scrollable view. * @return ViewAction */ - public static ViewAction scrollToEdge(final int edge) { + public static ViewAction scrollToEdge(final int edge, double startOffsetPercentX, double startOffsetPercentY) { + final Float _startOffsetPercentX = startOffsetPercentX < 0 ? null : (float) startOffsetPercentX; + final Float _startOffsetPercentY = startOffsetPercentY < 0 ? null : (float) startOffsetPercentY; + return actionWithAssertions(new ViewAction() { @Override public Matcher getConstraints() { @@ -97,7 +102,7 @@ public String getDescription() { public void perform(UiController uiController, View view) { try { for (int i = 0; i < 100; i++) { - ScrollHelper.performOnce(uiController, view, edge); + ScrollHelper.performOnce(uiController, view, edge, _startOffsetPercentX, _startOffsetPercentY); } throw new DetoxRuntimeException("Scrolling a lot without reaching the edge: force-breaking the loop"); } catch (ScrollEdgeException e) { @@ -112,8 +117,8 @@ public void perform(UiController uiController, View view) { * * @param direction Direction to scroll (see {@link MotionDir}) * @param amountInDP Density Independent Pixels - * @param startOffsetPercentX Percentage denoting where X-swipe should start, with respect to the scrollable view. - * @param startOffsetPercentY Percentage denoting where Y-swipe should start, with respect to the scrollable view. + * @param startOffsetPercentX Percentage denoting where the scroll should start from on the X-axis, with respect to the scrollable view. + * @param startOffsetPercentY Percentage denoting where the scroll should start from on the Y-axis, with respect to the scrollable view. */ public static ViewAction scrollInDirection(final int direction, final double amountInDP, double startOffsetPercentX, double startOffsetPercentY) { final Float _startOffsetPercentX = startOffsetPercentX < 0 ? null : (float) startOffsetPercentX; @@ -129,8 +134,8 @@ public static ViewAction scrollInDirection(final int direction, final double amo * * @param direction Direction to scroll (see {@link MotionDir}) * @param amountInDP Density Independent Pixels - * @param startOffsetPercentX Percentage denoting where X-swipe should start, with respect to the scrollable view. - * @param startOffsetPercentY Percentage denoting where Y-swipe should start, with respect to the scrollable view. + * @param startOffsetPercentX Percentage denoting where the scroll should start from on the X-axis, with respect to the scrollable view. + * @param startOffsetPercentY Percentage denoting where the scroll should start from on the Y-axis, with respect to the scrollable view. */ public static ViewAction scrollInDirectionStaleAtEdge(final int direction, final double amountInDP, double startOffsetPercentX, double startOffsetPercentY) { final Float _startOffsetPercentX = startOffsetPercentX < 0 ? null : (float) startOffsetPercentX; diff --git a/detox/android/detox/src/main/java/com/wix/detox/espresso/scroll/ScrollHelper.java b/detox/android/detox/src/main/java/com/wix/detox/espresso/scroll/ScrollHelper.java index bfca25cb1f..ac08100841 100644 --- a/detox/android/detox/src/main/java/com/wix/detox/espresso/scroll/ScrollHelper.java +++ b/detox/android/detox/src/main/java/com/wix/detox/espresso/scroll/ScrollHelper.java @@ -42,8 +42,8 @@ private ScrollHelper() { * * @param direction Direction to scroll (see {@link MotionDir}) * @param amountInDP Density Independent Pixels - * @param startOffsetPercentX Percentage denoting where X-swipe should start, with respect to the scrollable view. Null means select automatically. - * @param startOffsetPercentY Percentage denoting where Y-swipe should start, with respect to the scrollable view. Null means select automatically. + * @param startOffsetPercentX Percentage denoting where the scroll should start from on the X-axis, with respect to the scrollable view. Null means select automatically. + * @param startOffsetPercentY Percentage denoting where the scroll should start from on the Y-axis, with respect to the scrollable view. Null means select automatically. */ public static void perform(UiController uiController, View view, @MotionDir int direction, double amountInDP, Float startOffsetPercentX, Float startOffsetPercentY) throws ScrollEdgeException { final int amountInPx = DeviceDisplay.convertDpiToPx(amountInDP); @@ -64,10 +64,12 @@ public static void perform(UiController uiController, View view, @MotionDir int * of the screen.) * * @param direction Direction to scroll (see {@link @MotionDir}) + * @param startOffsetPercentX Percentage denoting where the scroll should start from on the X-axis, with respect to the scrollable view. Null means select automatically. + * @param startOffsetPercentY Percentage denoting where the scroll should start from on the Y-axis, with respect to the scrollable view. Null means select automatically. */ - public static void performOnce(UiController uiController, View view, @MotionDir int direction) throws ScrollEdgeException { + public static void performOnce(UiController uiController, View view, @MotionDir int direction, Float startOffsetPercentX, Float startOffsetPercentY) throws ScrollEdgeException { final int scrollableRangePx = getViewSafeScrollableRangePix(view, direction); - scrollOnce(uiController, view, direction, scrollableRangePx, null, null); + scrollOnce(uiController, view, direction, scrollableRangePx, startOffsetPercentX, startOffsetPercentY); } private static void scrollOnce(UiController uiController, View view, @MotionDir int direction, int userAmountPx, Float startOffsetPercentX, Float startOffsetPercentY) throws ScrollEdgeException { diff --git a/detox/detox.d.ts b/detox/detox.d.ts index 86314cccdc..b39ae586c9 100644 --- a/detox/detox.d.ts +++ b/detox/detox.d.ts @@ -1363,10 +1363,13 @@ declare global { /** * Scroll to edge. - * @example await element(by.id('scrollView')).scrollTo('bottom'); + * @param edge - left|right|top|bottom + * @param startPositionX - the X starting scroll position, in percentage; valid input: `[0.0, 1.0]`, `NaN`; default: `NaN`—choose the best value automatically + * @param startPositionY - the Y starting scroll position, in percentage; valid input: `[0.0, 1.0]`, `NaN`; default: `NaN`—choose the best value automatically + * @example await element(by.id('scrollView')).scrollTo('bottom', NaN, 0.85); * @example await element(by.id('scrollView')).scrollTo('top'); */ - scrollTo(edge: Direction): Promise; + scrollTo(edge: Direction, startPositionX?: number, startPositionY?: number): Promise; /** * Adjust slider to position. diff --git a/detox/ios/Detox/Actions/UIScrollView+DetoxActions.h b/detox/ios/Detox/Actions/UIScrollView+DetoxActions.h index c2c0638fd0..5719a0ca57 100644 --- a/detox/ios/Detox/Actions/UIScrollView+DetoxActions.h +++ b/detox/ios/Detox/Actions/UIScrollView+DetoxActions.h @@ -13,6 +13,8 @@ NS_ASSUME_NONNULL_BEGIN @interface UIScrollView (DetoxActions) - (void)dtx_scrollToEdge:(UIRectEdge)edge NS_SWIFT_NAME(dtx_scroll(to:)); +- (void)dtx_scrollToEdge:(UIRectEdge)edge + normalizedStartingPoint:(CGPoint)normalizedStartingPoint; - (void)dtx_scrollWithOffset:(CGPoint)offset; - (void)dtx_scrollWithOffset:(CGPoint)offset normalizedStartingPoint:(CGPoint)normalizedStartingPoint NS_SWIFT_NAME(dtx_scroll(withOffset:normalizedStartingPoint:)); diff --git a/detox/ios/Detox/Actions/UIScrollView+DetoxActions.m b/detox/ios/Detox/Actions/UIScrollView+DetoxActions.m index 5bcd147473..d78c470e48 100644 --- a/detox/ios/Detox/Actions/UIScrollView+DetoxActions.m +++ b/detox/ios/Detox/Actions/UIScrollView+DetoxActions.m @@ -83,7 +83,7 @@ @implementation UIScrollView (DetoxActions) [self setContentOffset:pointMakeMacro(target) animated:YES]; \ [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:[[self valueForKeyPath:@"animation.duration"] doubleValue] + 0.05]]; -- (void)dtx_scrollToEdge:(UIRectEdge)edge +- (CGPoint)_edgeToNormalizedEdge:(UIRectEdge)edge { CGPoint normalizedEdge; switch (edge) { @@ -100,10 +100,19 @@ - (void)dtx_scrollToEdge:(UIRectEdge)edge normalizedEdge = CGPointMake(1, 0); break; default: + normalizedEdge= CGPointMake(0, 0); DTXAssert(NO, @"Incorect edge provided."); - return; } - + return normalizedEdge; +} + + +- (void)dtx_scrollToEdge:(UIRectEdge)edge +{ + CGPoint normalizedEdge = [self _edgeToNormalizedEdge:edge]; + if(normalizedEdge.x == 0 && normalizedEdge.y == 0) + return; + [self _dtx_scrollToNormalizedEdge:normalizedEdge]; } @@ -121,6 +130,23 @@ - (void)_dtx_scrollToNormalizedEdge:(CGPoint)edge [self _dtx_scrollWithOffset:CGPointMake(- edge.x * CGFLOAT_MAX, - edge.y * CGFLOAT_MAX) normalizedStartingPoint:CGPointMake(NAN, NAN) strict:NO]; } +- (void)dtx_scrollToEdge:(UIRectEdge)edge + normalizedStartingPoint:(CGPoint)normalizedStartingPoint +{ + CGPoint normalizedEdge = [self _edgeToNormalizedEdge:edge]; + if(normalizedEdge.x == 0 && normalizedEdge.y == 0) + return; + + [self _dtx_scrollToNormalizedEdge:normalizedEdge normalizedStartingPoint: normalizedStartingPoint ]; +} + +- (void)_dtx_scrollToNormalizedEdge:(CGPoint)edge + normalizedStartingPoint:(CGPoint)normalizedStartingPoint +{ + [self _dtx_scrollWithOffset:CGPointMake(- edge.x * CGFLOAT_MAX, - edge.y * CGFLOAT_MAX) normalizedStartingPoint:normalizedStartingPoint strict:NO]; +} + + DTX_ALWAYS_INLINE static NSString* _DTXScrollDirectionDescriptionWithOffset(CGPoint offset) { diff --git a/detox/ios/Detox/Invocation/Action.swift b/detox/ios/Detox/Invocation/Action.swift index a4ac09db30..1a4470d741 100644 --- a/detox/ios/Detox/Invocation/Action.swift +++ b/detox/ios/Detox/Invocation/Action.swift @@ -129,6 +129,15 @@ class Action : CustomStringConvertible { } } + func startPosition(forIndex index: Int, in params: [Any]?) -> Double { + guard params?.count ?? 0 > index, + let param = params?[index] as? Double, + param.isNaN == false else { + return Double.nan + } + return param + } + var description: String { let paramsDescription: String if let params = params { @@ -434,9 +443,13 @@ class ScrollToEdgeAction : Action { fatalError("Unknown scroll direction") break; } - - element.scroll(to: targetEdge) - + + let startPositionX = startPosition(forIndex: 1, in: params) + let startPositionY = startPosition(forIndex: 2, in: params) + let normalizedStartingPoint = CGPoint(x: startPositionX, y: startPositionY) + + element.scroll(to: targetEdge, normalizedStartingPoint: normalizedStartingPoint) + return nil } } @@ -487,18 +500,9 @@ class SwipeAction : Action { targetNormalizedOffset.x *= CGFloat(appliedPercentage) targetNormalizedOffset.y *= CGFloat(appliedPercentage) - let startPositionX : Double - if params?.count ?? 0 > 3, let param2 = params?[3] as? Double, param2.isNaN == false { - startPositionX = param2 - } else { - startPositionX = Double.nan - } - let startPositionY : Double - if params?.count ?? 0 > 4, let param3 = params?[4] as? Double, param3.isNaN == false { - startPositionY = param3 - } else { - startPositionY = Double.nan - } + + let startPositionX = startPosition(forIndex: 3, in: params) + let startPositionY = startPosition(forIndex: 4, in: params) let normalizedStartingPoint = CGPoint(x: startPositionX, y: startPositionY) element.swipe(normalizedOffset: targetNormalizedOffset, velocity: velocity, normalizedStartingPoint: normalizedStartingPoint) diff --git a/detox/ios/Detox/Invocation/Element.swift b/detox/ios/Detox/Invocation/Element.swift index 5570ab5ba5..378608b8af 100644 --- a/detox/ios/Detox/Invocation/Element.swift +++ b/detox/ios/Detox/Invocation/Element.swift @@ -140,10 +140,13 @@ class Element : NSObject { view.dtx_pinch(withScale: scale, velocity: velocity, angle: angle) } - func scroll(to edge: UIRectEdge) { + func scroll(to edge: UIRectEdge, normalizedStartingPoint: CGPoint? = nil) { let scrollView = extractScrollView() - - scrollView.dtx_scroll(to: edge) + if let normalizedStartingPoint = normalizedStartingPoint { + scrollView.dtx_scroll(to: edge, normalizedStarting: normalizedStartingPoint) + } else { + scrollView.dtx_scroll(to: edge) + } } func scroll(withOffset offset: CGPoint, normalizedStartingPoint: CGPoint? = nil) { diff --git a/detox/src/android/actions/native.js b/detox/src/android/actions/native.js index 601bef562d..7b28efbed5 100644 --- a/detox/src/android/actions/native.js +++ b/detox/src/android/actions/native.js @@ -81,10 +81,10 @@ class ScrollAmountStopAtEdgeAction extends Action { } class ScrollEdgeAction extends Action { - constructor(edge) { + constructor(edge, startPositionX = -1, startPositionY = -1) { super(); - this._call = invoke.callDirectly(DetoxActionApi.scrollToEdge(edge)); + this._call = invoke.callDirectly(DetoxActionApi.scrollToEdge(edge, startPositionX, startPositionY)); } } diff --git a/detox/src/android/core/NativeElement.js b/detox/src/android/core/NativeElement.js index 1ecbfa7597..d493a83e8c 100644 --- a/detox/src/android/core/NativeElement.js +++ b/detox/src/android/core/NativeElement.js @@ -93,12 +93,12 @@ class NativeElement { return await new ActionInteraction(this._invocationManager, this._matcher, action, traceDescription).execute(); } - async scrollTo(edge) { + async scrollTo(edge, startPositionX, startPositionY) { // override the user's element selection with an extended matcher that looks for UIScrollView children this._matcher = this._matcher._extendToDescendantScrollViews(); - const action = new actions.ScrollEdgeAction(edge); - const traceDescription = actionDescription.scrollTo(edge); + const action = new actions.ScrollEdgeAction(edge, startPositionX, startPositionY); + const traceDescription = actionDescription.scrollTo(edge, startPositionX, startPositionY); return await new ActionInteraction(this._invocationManager, this._matcher, action, traceDescription).execute(); } diff --git a/detox/src/android/espressoapi/DetoxAction.js b/detox/src/android/espressoapi/DetoxAction.js index 6ed2d213f8..df5c313900 100644 --- a/detox/src/android/espressoapi/DetoxAction.js +++ b/detox/src/android/espressoapi/DetoxAction.js @@ -68,8 +68,10 @@ class DetoxAction { }; } - static scrollToEdge(edge) { + static scrollToEdge(edge, startOffsetPercentX, startOffsetPercentY) { if (typeof edge !== "string") throw new Error("edge should be a string, but got " + (edge + (" (" + (typeof edge + ")")))); + if (typeof startOffsetPercentX !== "number") throw new Error("startOffsetPercentX should be a number, but got " + (startOffsetPercentX + (" (" + (typeof startOffsetPercentX + ")")))); + if (typeof startOffsetPercentY !== "number") throw new Error("startOffsetPercentY should be a number, but got " + (startOffsetPercentY + (" (" + (typeof startOffsetPercentY + ")")))); return { target: { type: "Class", @@ -79,6 +81,12 @@ class DetoxAction { args: [{ type: "Integer", value: sanitize_android_edge(edge) + }, { + type: "Double", + value: startOffsetPercentX + }, { + type: "Double", + value: startOffsetPercentY }] }; } diff --git a/detox/src/ios/expectTwo.js b/detox/src/ios/expectTwo.js index b2153b4386..f89b7f26ae 100644 --- a/detox/src/ios/expectTwo.js +++ b/detox/src/ios/expectTwo.js @@ -247,10 +247,13 @@ class Element { return this.withAction('scroll', traceDescription, pixels, direction, startPositionX, startPositionY); } - scrollTo(edge) { + scrollTo(edge, startPositionX = NaN, startPositionY = NaN) { if (!['left', 'right', 'top', 'bottom'].some(option => option === edge)) throw new Error('edge should be one of [left, right, top, bottom], but got ' + edge); - const traceDescription = actionDescription.scrollTo(edge); - return this.withAction('scrollTo', traceDescription, edge); + if (typeof startPositionX !== 'number') throw new Error('startPositionX should be a number, but got ' + (startPositionX + (' (' + (typeof startPositionX + ')')))); + if (typeof startPositionY !== 'number') throw new Error('startPositionY should be a number, but got ' + (startPositionY + (' (' + (typeof startPositionY + ')')))); + + const traceDescription = actionDescription.scrollTo(edge, startPositionX, startPositionY); + return this.withAction('scrollTo', traceDescription, edge, startPositionX, startPositionY); } swipe(direction, speed = 'fast', normalizedSwipeOffset = NaN, normalizedStartingPointX = NaN, normalizedStartingPointY = NaN) { diff --git a/detox/src/ios/expectTwoApiCoverage.test.js b/detox/src/ios/expectTwoApiCoverage.test.js index 2980bbdb4d..442366d75b 100644 --- a/detox/src/ios/expectTwoApiCoverage.test.js +++ b/detox/src/ios/expectTwoApiCoverage.test.js @@ -181,6 +181,8 @@ describe('expectTwo API Coverage', () => { await expectToThrow(() => e.element(e.by.id('someId')).scrollTo(0)); await expectToThrow(() => e.element(e.by.id('someId')).scrollTo('noDirection')); + await expectToThrow(() => e.element(e.by.id('someId')).scrollTo('top','Nan', 0.5)); + await expectToThrow(() => e.element(e.by.id('someId')).scrollTo('top', 0.5, 'Nan')); await expectToThrow(() => e.element(e.by.id('someId')).swipe(4, 'fast')); await expectToThrow(() => e.element(e.by.id('someId')).swipe('left', 'fast', 20)); @@ -266,6 +268,7 @@ describe('expectTwo API Coverage', () => { await e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).scroll(50, 'down'); await e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).scroll(50, 'down', 0, 0); await e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).scrollTo('left'); + await e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).scrollTo('left', 0.1, 0.1); await e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).swipe('left'); await e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).swipe('left', 'fast'); await e.waitFor(e.element(e.by.id('id'))).toBeVisible().whileElement(e.by.id('id2')).swipe('left', 'slow', 0.1); diff --git a/detox/src/utils/invocationTraceDescriptions.js b/detox/src/utils/invocationTraceDescriptions.js index a9168327ae..63969bf635 100644 --- a/detox/src/utils/invocationTraceDescriptions.js +++ b/detox/src/utils/invocationTraceDescriptions.js @@ -12,8 +12,9 @@ module.exports = { pinchWithAngle: (direction, speed, angle) => `pinch with direction ${direction}, speed ${speed}, and angle ${angle}`, replaceText: (value) => `replace input text: "${value}"`, scroll: (amount, direction, startPositionX, startPositionY) => - `scroll ${amount} pixels ${direction}${startPositionX !== undefined && startPositionY !== undefined ? ` from normalized position (${startPositionX}, ${startPositionY})` : ''}`, - scrollTo: (edge) => `scroll to ${edge}`, + `scroll ${amount} pixels ${direction}${startPositionX !== undefined || startPositionY !== undefined ? ` from normalized position (${startPositionX}, ${startPositionY})` : ''}`, + scrollTo: (edge, startPositionX, startPositionY) => + `scroll to ${edge} ${startPositionX !== undefined || startPositionY !== undefined ? ` from normalized position (${startPositionX}, ${startPositionY})` : ''}`, scrollToIndex: (index) => `scroll to index #${index}`, setColumnToValue: (column, value) => `set column ${column} to value ${value}`, setDatePickerDate: (dateString, dateFormat) => `set date picker date to ${dateString} using format ${dateFormat}`, diff --git a/detox/test/e2e/03.actions-scroll.test.js b/detox/test/e2e/03.actions-scroll.test.js index e4cb32f444..6615d6ea4b 100644 --- a/detox/test/e2e/03.actions-scroll.test.js +++ b/detox/test/e2e/03.actions-scroll.test.js @@ -46,6 +46,32 @@ describe('Actions - Scroll', () => { await expect(element(by.text('HText1'))).toBeVisible(); }); + it('should scroll to edge from a custom start-position ratio', async () => { + await expect(element(by.text('Text12'))).not.toBeVisible(); + await element(by.id('toggleScrollOverlays')).tap(); + await element(by.id('ScrollView161')).scrollTo('bottom', 0.2, 0.4); + await element(by.id('toggleScrollOverlays')).tap(); + await expect(element(by.text('Text12'))).toBeVisible(); + + await element(by.id('toggleScrollOverlays')).tap(); + await element(by.id('ScrollView161')).scrollTo('top', 0.8, 0.6); + await element(by.id('toggleScrollOverlays')).tap(); + await expect(element(by.text('Text1'))).toBeVisible(); + }); + + it('should scroll to edge horizontally from a custom start-position ratio', async () => { + await expect(element(by.text('HText8'))).not.toBeVisible(); + await element(by.id('toggleScrollOverlays')).tap(); + await element(by.id('ScrollViewH')).scrollTo('right', 0.8, 0.6); + await element(by.id('toggleScrollOverlays')).tap(); + await expect(element(by.text('HText8'))).toBeVisible(); + + await element(by.id('toggleScrollOverlays')).tap(); + await element(by.id('ScrollViewH')).scrollTo('left',0.2, 0.4); + await element(by.id('toggleScrollOverlays')).tap(); + await expect(element(by.text('HText1'))).toBeVisible(); + }); + it('should scroll from a custom start-position ratio', async () => { await expect(element(by.text('Text12'))).not.toBeVisible(); await element(by.id('toggleScrollOverlays')).tap(); diff --git a/docs/api/actions.md b/docs/api/actions.md index c249c0aa67..4c6d9f2891 100644 --- a/docs/api/actions.md +++ b/docs/api/actions.md @@ -144,15 +144,17 @@ Continuously scrolls the scroll element until the specified expectation is resol await waitFor(element(by.text('Text5'))).toBeVisible().whileElement(by.id('ScrollView630')).scroll(50, 'down'); ``` -### `scrollTo(edge)` +### `scrollTo(edge[, startPositionX, startPositionY])` Simulates a scroll to the specified edge. -`edge`—the edge to scroll to (valid input: `"left"`/`"right"`/`"top"`/`"bottom"`) +`edge`—the edge to scroll to (valid input: `"left"`/`"right"`/`"top"`/`"bottom"`)
+`startPositionX`—the normalized x percentage of the element to use as scroll start point (optional, valid input: \[0.0, 1.0], `NaN`—choose an optimal value automatically, default is `NaN`)
+`startPositionY`—the normalized y percentage of the element to use as scroll start point (optional, valid input: \[0.0, 1.0], `NaN`—choose an optimal value automatically, default is `NaN`) ```js await element(by.id('scrollView')).scrollTo('bottom'); -await element(by.id('scrollView')).scrollTo('top'); +await element(by.id('scrollView')).scrollTo('top', NaN, 0.2); ``` ### `typeText(text)`