2222 * SOFTWARE.
2323 */
2424
25+ using System . Reflection ;
2526using NUnit . Framework . Interfaces ;
2627using NUnit . Framework . Internal ;
2728using NUnit . Framework . Internal . Commands ;
@@ -36,14 +37,24 @@ namespace Microsoft.Playwright.Tests;
3637/// Enables decorating test facts with information about the corresponding test in the upstream repository.
3738/// </summary>
3839[ AttributeUsage ( AttributeTargets . Method , AllowMultiple = true ) ]
39- public class PlaywrightTestAttribute : TestAttribute , IApplyToContext , IApplyToTest , IWrapSetUpTearDown
40+ public class PlaywrightTestAttribute : TestAttribute , IWrapSetUpTearDown
4041{
41- private readonly CancelAfterAttribute _cancelAfterAttribute = new ( TestConstants . DefaultTestTimeout ) ;
42+ private readonly int ? _timeout ;
4243
4344 public PlaywrightTestAttribute ( )
4445 {
4546 }
4647
48+ public PlaywrightTestAttribute ( int timeout ) : this ( )
49+ {
50+ _timeout = timeout ;
51+ }
52+
53+ public PlaywrightTestAttribute ( string fileName , string nameOfTest , int timeout ) : this ( fileName , nameOfTest )
54+ {
55+ _timeout = timeout ;
56+ }
57+
4758 /// <summary>
4859 /// Creates a new instance of the attribute.
4960 /// </summary>
@@ -60,11 +71,6 @@ public PlaywrightTestAttribute(string fileName, string nameOfTest)
6071 /// </summary>
6172 public string FileName { get ; }
6273
63- /// <summary>
64- /// Returns the trimmed file name.
65- /// </summary>
66- public string TrimmedName => FileName . Substring ( 0 , FileName . IndexOf ( '.' ) ) ;
67-
6874 /// <summary>
6975 /// The name of the test, the decorated code is based on.
7076 /// </summary>
@@ -75,29 +81,14 @@ public PlaywrightTestAttribute(string fileName, string nameOfTest)
7581 /// </summary>
7682 public string Describe { get ; }
7783
78- public void ApplyToContext ( TestExecutionContext context )
79- {
80- if ( context . TestCaseTimeout == 0 )
81- {
82- ( _cancelAfterAttribute as IApplyToContext ) . ApplyToContext ( context ) ;
83- }
84- }
85-
86- public new void ApplyToTest ( Test test )
87- {
88- base . ApplyToTest ( test ) ;
89- if ( TestExecutionContext . CurrentContext . TestCaseTimeout == 0 )
90- {
91- _cancelAfterAttribute . ApplyToTest ( test ) ;
92- }
93- }
9484 /// <summary>
9585 /// Wraps the current test command in a <see cref="UnobservedTaskExceptionCommand"/>.
9686 /// </summary>
9787 /// <param name="command">the test command</param>
9888 /// <returns>the wrapped test command</returns>
9989 public TestCommand Wrap ( TestCommand command )
10090 {
91+ command = new TimeoutCommand ( command , _timeout ?? TestConstants . DefaultTestTimeout ) ;
10192 if ( Environment . GetEnvironmentVariable ( "CI" ) != null )
10293 {
10394 command = new RetryCommand ( command , 3 ) ;
@@ -121,7 +112,7 @@ public override TestResult Execute(TestExecutionContext context)
121112 try
122113 {
123114 innerCommand . Execute ( context ) ;
124- if ( context . CurrentResult . ResultState == ResultState . Success )
115+ if ( context . CurrentResult . ResultState == ResultState . Success || context . CurrentResult . ResultState == ResultState . Skipped || context . CurrentResult . ResultState == ResultState . Ignored )
125116 {
126117 isPassed = true ;
127118 break ;
@@ -205,4 +196,71 @@ private void UnobservedTaskException(object sender, UnobservedTaskExceptionEvent
205196 _unobservedTaskExceptions . Add ( e . Exception ) ;
206197 }
207198 }
199+
200+ public class TimeoutCommand : BeforeAndAfterTestCommand
201+ {
202+ private readonly int _timeout ;
203+
204+ internal TimeoutCommand ( TestCommand innerCommand , int timeout ) : base ( innerCommand )
205+ {
206+ _timeout = timeout ;
207+ }
208+
209+ public override TestResult Execute ( TestExecutionContext context )
210+ {
211+ try
212+ {
213+ using ( new TestExecutionContext . IsolatedContext ( ) )
214+ {
215+ var testExecution = Task . Run ( ( ) => innerCommand . Execute ( TestExecutionContext . CurrentContext ) ) ;
216+ var timedOut = Task . WaitAny ( [ testExecution ] , _timeout ) == - 1 ;
217+
218+ if ( timedOut )
219+ {
220+ context . CurrentResult . SetResult (
221+ ResultState . Failure ,
222+ $ "Test exceeded Timeout value of { _timeout } ms") ;
223+ // When the timeout is reached the TearDown methods are not called. This is a best-effort
224+ // attempt to call them and close the browser / http server.
225+ foreach ( var tearDown in GetHackyTearDownMethods ( context ) )
226+ {
227+ tearDown ( ) ;
228+ }
229+ }
230+ else
231+ {
232+ context . CurrentResult = testExecution . GetAwaiter ( ) . GetResult ( ) ;
233+ }
234+ }
235+ }
236+ catch ( Exception exception )
237+ {
238+ context . CurrentResult . RecordException ( exception , FailureSite . Test ) ;
239+ }
240+
241+ return context . CurrentResult ;
242+ }
243+
244+ private Action [ ] GetHackyTearDownMethods ( TestExecutionContext context )
245+ {
246+ var methods = new List < Action > ( ) ;
247+ foreach ( var method in new string [ ] { "WorkerTeardown" , "BrowserTearDown" } )
248+ {
249+ var methodFun = context . CurrentTest . Method . MethodInfo . DeclaringType
250+ . GetMethod ( method , BindingFlags . Instance | BindingFlags . Public | BindingFlags . NonPublic ) ;
251+ if ( methodFun != null )
252+ {
253+ methods . Add ( ( ) =>
254+ {
255+ var maybeTask = methodFun . Invoke ( context . TestObject , null ) ;
256+ if ( maybeTask is Task task )
257+ {
258+ task . GetAwaiter ( ) . GetResult ( ) ;
259+ }
260+ } ) ;
261+ }
262+ }
263+ return methods . ToArray ( ) ;
264+ }
265+ }
208266}
0 commit comments