TestComplete Tips

请来信索取最新版本(Email:quicktest##qq.com( 请把##改为@ ))

 

1、如何在TestComplete中用Jscript的类封装窗口定义

参考:

《TestComplete ( JScript ): Making windows definitions using wrapper classes》

http://autotestgroup.com/en/blog/69.html

 

 

2、如何控制鼠标进行拖拽操作?

function DragDrop(obj, deltaX, deltaY)

{

  var iX = obj.ScreenLeft + obj.Width/2;

  var iY = obj.ScreenTop + obj.Height/2;

  Log.Picture(obj.Picture(), "Object to be moved");

  obj = Sys.Desktop.ObjectFromPoint(iX + deltaX, iY + deltaY);

  Sys.Desktop.MouseDown(VK_LBUTTON, iX, iY);

  obj.HoverMouse(obj.Width/2, obj.Height/2);

  Sys.Desktop.MouseUp(VK_LBUTTON, iX + deltaX, iY + deltaY); 

}

 

function Test3()

{

  var w1 = Sys.Process("Explorer").Window("Shell_TrayWnd").Window("ToolbarWindow32", "Quick Launch");

  DragDrop(w1, -30, -20);

}

 

参考:

《TestComplete: when method Drag doesn't work》

http://autotestgroup.com/en/blog/61.html

 

 

 

3、如何在TestComplete中让Jscript的typeof返回“date”、“array”类型?

 

 

function _typeof(value)

{

  switch(typeof(value))

  { 

    case "string":

      return (value.match(/\d{1,2}[\/.-]\d{1,2}[\/.-]\d{2,4}/) != null) ? "date" : "string";

      break;

    case "object":

      try

      { value.join() }

      catch(e)

      {

        if(e.number == -2146827850)

          return "object"

        else

          throw e;

      }

      return "array";

      break;

    default:

      return typeof(value);

  } 

}

 

参考:

《TestComplete:improving Jscript typeof operator》

http://autotestgroup.com/en/blog/46.html

 

 

4、如何在延迟脚本执行时显示剩余时间?

 

 

function Sleep(iSeconds)

{

 i = iSeconds;

 while(i > 0)

 {

    BuiltIn.Delay(500);

    Indicator.PushText("Delaying script execution for " + iSeconds + " seconds. " + i + " seconds left");

 

    BuiltIn.Delay(500);

    i -= 1;

 }

 Indicator.Clear();

}

 

 

参考:

《TestComplete:delaying script execution》

http://autotestgroup.com/en/blog/42.html

 

 

5、如何定时检查某个窗口是否出现,如果出现则关闭它?

 

 

// TestComplete JScript

function KillCalculator()

{

 if(Sys.WaitProcess("calc", 1, 1).Exists)

 {

    Log.Message("Calculator has been found");

    Sys.Process("calc").Terminate();

 }

}

 

function test_timer()

{

 Utils.Timers.Add(500, "MyUnit.KillCalculator", true);

 BuiltIn.Delay(60000);

}

 

参考:

《TestComplete:Closing Unexpected Windows》

http://autotestgroup.com/en/blog/28.html

 

 

6、如何用TC处理IE的文件下载窗口?

 

function MegaClick( obj )

{

   for( i = 1; i <= 2; i++ )

  {

     if( obj.Focused )

      obj.Parent.Keys( "[Tab]" );

     while( !obj.Focused )

      obj.Parent.Keys( "[Tab]" );

  }

  obj.Click();

}

 

 

参考:

《A strange behavior of the File Download window》

http://autotestgroup.com/en/blog/23.html

 

 

7、TestComplete的ClickButton()方法

 

录制按钮点击时一般TestComplete会生成ClickButton方法,例如:

var w = Sys.Process("WindowsApplication3"). WinFormsObject("Form1");

w.WinFormsObject("button1").ClickButton();

 

建议替换成Click(),因为标准按钮可能会被替换成自定义的控件或第三方的控件,而使用了相同的名字,这种情况下ClickButton可能会工作不正常。

 

参考:

http://autotestgroup.com/en/blog/17.html

 

 

8、在TestComplete中如何读写Excel?

 

用Jscript可以这样写:

function TestExcelClass()

{

  // specifying excel file name and deleting it, if it is already exists

  var excel = new ExcelClass("C:\\temp.xls");

  if(Utilities.FileExists(excel.FileName))

  {

    Utilities.DeleteFile(excel.FileName);

  }

 

  // creating new Excel file with the specified name and predefined sheets

  var arrSheets = new Array("MySheet1", "MySheet2", "MySheet3", "MySheet4");

  excel.CreateFile(null, arrSheets);

 

  // writing data to the created Excel file by different ways

  excel.Write("MySheet1", 1, 1, "text in row 1, column 1");

  excel.Write("MySheet1", 1, "B", "text in row 1, column 'B'");

  excel.WriteCell("MySheet1", "C1", "text in cell 'C1'");

 

  // specifying data for "matrix writing"

  var arrRow1 = new Array("text1", "text2", "text3");

  var arrRow2 = new Array("text4", "text5", "text6");

  var arrMatrix = new Array(arrRow1, arrRow2);

 

  excel.WriteMatrix("MySheet1", 3, "A", arrMatrix);

 

  // reading written data using different methods

  Log.Message("Text in cell A1", excel.ReadCell("MySheet1", "A1"));

  Log.Message("Text in cell B1", excel.Read("MySheet1", 1, 2));

  Log.Message("Text in cell C1", excel.Read("MySheet1", 1, "C"));

 

  var arrNew1 = excel.ReadMatrix("MySheet1", 3, "A", 4, "C");

 

  Log.Message("Matrix data", arrNew1[0].join("  ") + "\n" + arrNew1[1].join("  "));

  var arrNew2 = excel.ReadCells("MySheet1", "A1", "C1", "A4", "C4");

  Log.Message("Data from several cells", arrNew2.join("\n"));

}

 

 

ExcelClass类的代码如下:

// Automated Testing Service Group

// Excel class

 

// Global variables

var oExcel = Sys.OleObject("Excel.Application");

 

function ExcelClass(sFileName)

{

  // - - - - variables

  var oWorkbook, oSheet, i;

 

  // - - - - properties

  this.FileName     = (sFileName == null? "" : sFileName);

  this.DefaultSheet = "Sheet1";

  this.MaxColumns   = 256;

  this.MaxRows      = 65536;

  oExcel.ScreenUpdating = false;

  //oExcel.DisplayAlerts = false;

  //oExcel.Interactive = false;

 

  // - - - - public methods

  this.CreateFile = function(sFileName, aSheets)

  {

    if(sFileName != null)

    { this.FileName = sFileName }

 

    if(Utilities.FileExists(this.FileName))

    {

      Log.Warning("File is already exist", "You are trying to create a new file which is already exist\n" + this.FileName);

      return true;

    }

 

    try

    {

      oWorkbook = oExcel.Workbooks.Add();

      if(aSheets != null)

      {

        Log.Message("Creating " + aSheets.length + " new sheets");

        if(aSheets.length > oWorkbook.Sheets.Count)

        {

            for(i = oWorkbook.Sheets.Count+1; i <= aSheets.length; i++)

            { oWorkbook.Sheets.Add(); }

        }

        for(i in aSheets)

        {

          oWorkbook.Sheets(Number(i)+1).Name = aSheets[i];

        }

      }

      oWorkbook.SaveAs(this.FileName);

      oWorkbook.Close();

      //oExcel.Quit();

    }

    catch(e)

    {

      Log.Error("An exception occured, see remarks", "Exception number: " + e.number + "\nException description: " + e.description)

    }

  }

 

  this.Write = function(sSheet, iRow, Col, value)

  {

    if(sSheet == null) sSheet = this.DefaultSheet;

    var iCol = typeof(Col) == "string" ? this.ConvertColName(Col) : Col;

 

    try

    {

      oWorkbook = oExcel.Workbooks.Open(this.FileName);

      oWorkbook.Sheets(sSheet).Cells(iRow, iCol).value = value;

      oWorkbook.Save();

      oWorkbook.Close();

    }

    catch(e)

    {

      Log.Error("An exception occured, see remarks", "Exception number: " + e.number + "\nException description: " + e.description);

    }

  }

 

  this.Read = function(sSheet, iRow, Col)

  {

    var iCol = typeof(Col) == "string" ? this.ConvertColName(Col) : Col;

 

    oWorkbook = oExcel.Workbooks.Open(this.FileName);

    var value = oWorkbook.Sheets(sSheet).Cells(iRow, iCol).value;

    oWorkbook.Close();

    return value;

  }

 

  this.ReadRow = function(sSheet, iRow, iMaxColumns)

  {

    var aRet = new Array();

    var i;

 

    if(iMaxColumns == null) iMaxColumns = this.MaxColumns;

    if(sSheet == null) sSheet = this.DefaultSheet;

 

    oWorkbook = oExcel.Workbooks.Open(this.FileName);

    for(i = 1; i <= iMaxColumns; i++)

    {

      aRet.push(oWorkbook.Sheets(sSheet).Cells(iRow, i).value);

    }

    oWorkbook.Close();

 

    return aRet;

  }

 

  this.ReadCol = function(sSheet, Col, iMaxRows)

  {

    var aRet = new Array();

    var i;

 

    if(iMaxRows == null) iMaxRows = this.MaxRows;

    if(sSheet == null) sSheet = this.DefaultSheet;

    var iCol = typeof(Col) == "string" ? this.ConvertColName(Col) : Col;

 

    oWorkbook = oExcel.Workbooks.Open(this.FileName);

    for(i = 1; i <= iMaxRows; i++)

    {

      aRet.push(oWorkbook.Sheets(sSheet).Cells(i, iCol).value);

    }

    oWorkbook.Close();

 

    return aRet;

  }

 

  //

  this.ReadMatrix = function(sSheet, iRowStart, ColStart, iRowEnd, ColEnd)

  {

    var i, j;

    var arr  = new Array();

    var aRet = new Array();

    var iColStart = typeof(ColStart) == "string" ? this.ConvertColName(ColStart) : ColStart;

    var iColEnd = typeof(ColEnd) == "string" ? this.ConvertColName(ColEnd) : ColEnd;

 

    oWorkbook = oExcel.Workbooks.Open(this.FileName);

    for(i = iRowStart; i <= iRowEnd; i++)

    {

      for(j = iColStart; j <= iColEnd; j++)

      {

        arr.push(oWorkbook.Sheets(sSheet).Cells(i, j).value);

      }

      aRet.push(arr);

      arr = new Array();

    }

    oWorkbook.Close();

 

    return aRet;

  }

 

  this.WriteMatrix = function(sSheet, iRowStart, ColStart, aData)

  {

    var i, j;

    var iColStart = typeof(ColStart) == "string" ? this.ConvertColName(ColStart) : ColStart;

    var iCol = iColStart;

    var iRow = iRowStart;

 

    oWorkbook = oExcel.Workbooks.Open(this.FileName);

    for(i = 0; i < aData.length; i++)

    {

      for(j = 0; j < aData[i].length; j++)

      {

        oWorkbook.Sheets(sSheet).Cells(iRow, iCol).value = aData[i][j];

        iCol++;

      }

      iCol = iColStart;

      iRow++;

    }

    oWorkbook.Save();

    oWorkbook.Close();

 

  }

 

  //

  this.ReadCells = function(sSheet)

  {

    var aRet = new Array();

    var i;

 

    if(sSheet == null) sSheet = this.DefaultSheet;

 

    for(i = 1; i < arguments.length; i++)

    {

      aRet.push(this.Read(sSheet, this.ConvertCellName(arguments[i]).row, this.ConvertCellName(arguments[i]).col));

    }

 

    return aRet;

  }

 

  //

  this.WriteCell = function(sSheet, sCell, value)

  {

    var arr = this.ConvertCellName(sCell);

    this.Write(sSheet, arr.row, arr.col, value);

  }

 

  //

  this.ReadCell = function(sSheet, sCell)

  {

    var arr = this.ConvertCellName(sCell);

 

    return this.Read(sSheet, arr.row, arr.col);

  }

 

 

 

  // - - - - private methods

  this.ConvertCellName = function(sCellName)

  {

    var aRet = new Array();

    var i;

 

    for(i = 0; i < sCellName.length; i++)

    {

      if(sCellName.charCodeAt(i) < 64)

      {

        aRet.row = sCellName.substr(i, sCellName.length);

        aRet.col = this.ConvertColName(sCellName.substr(0, i));

        break;

      }

    }

 

    return aRet;

  }

 

  this.ConvertColName = function(sColName)

  {

    var iCol = 0;

 

    switch(sColName.length)

    {

      case 1:

        iCol = sColName.toUpperCase().charCodeAt(0) - 64;

        break;

      case 2:

        iCol = (sColName.toUpperCase().charCodeAt(0) - 64)*26 + (sColName.toUpperCase().charCodeAt(1) - 64);

        break;

      case 3:

        iCol = (sColName.toUpperCase().charCodeAt(0) - 64)*676 + (sColName.toUpperCase().charCodeAt(1) - 64)*26 + (sColName.toUpperCase().charCodeAt(2) - 64);

        break;

      case 4:

        iCol = (sColName.toUpperCase().charCodeAt(0) - 64)*17576 + (sColName.toUpperCase().charCodeAt(0) - 64)*676 + (sColName.toUpperCase().charCodeAt(1) - 64)*26 + (sColName.toUpperCase().charCodeAt(2) - 64);

        break;

      default:

        Log.Error("Column name '" + sColName + "' cannot be converted");

        break;

    }

 

    return iCol;

  }

}

 

 

参考:

http://autotestgroup.com/en/materials/16.html

 

 

9、在用TestComplete测试时如何自动统计代码覆盖率?

 

可以结合Ncover进行代码覆盖率的自动统计

 

参考:

http://docs.ncover.com/how-to/running-ncover-with-your-unit-testing-framework/testcomplete/

 

 

10、用Exists属性判断窗口是否存在时,如果窗口不存在会自动写入一条错误日志,如何避免,改为写入自己的日志?

 

用Exists属性判断窗口是否存在:

  Sys["Process"]("flight4a")["Window"]("#32770","Login", 1)["Exists"]

会自动写入错误日志:

Cannot obtain the window with the windowclass '#32770', window caption 'Login' and index 1.

 

 

改用WaitWindow:

function Test1()

{

  loginWindow = Sys["Process"]("flight4a")["WaitWindow"]("#32770","Login", 1)

  if(loginWindow["Exists"])

    Log["Message"]("Exists");

  else 

    Log["Error"]("NotExists")

}

 

 

11、加快TestComplete执行速度的Tips

是什么原因导致TestComplete的脚本执行速度不够快呢?这要从TestComplete的3个方面进行了解:

1、TC在查找对象树时,如果对象不存在,TC会等待一段时间看对象是否出现。

 

2、找到对象后,TC将分析对象的类型,然后给对象附加各种可能的方法和属性,这可能要消耗很多的时间,例如对于一个.NET控件对象,除了提取普通的属性和方法外,TC还会看控件对象是否暴露了MSAA、UI Automation等方面的属性和方法,有的话也会一并添加。

 

3、有时候TC的脚本执行速度会受到界面控件性能的影响,例如展开一个TreeView控件时,如果Item比较多,而且使用了一些动态效果来重画的话,则TC要等待这些动作完成才能执行下一条语句。

 

 

知道了TC的执行效率受制约的因素后,我们就可以考虑从以下几个方面对TC进行“加速”

 

1. 把TC中有些不需要的插件屏蔽掉。

2. 过滤掉那些不需要使用的应用程序进程。

3. 如果不需要的话,请把Debug Agent关闭。

4. 优化Wait选项和方法的使用。

5. 调整Delay选项。

6. 在操作系统中调整Double-Click的速度。

7. 关闭Windows的界面视觉效果。

8. 使用其他方法替代模拟用户操作,例如用Keys替代Sys.DeskTop.KeyDown和Sys.DeskTop.KeyUp。

9. 优化low-level 的录制脚本,剔除一些无用的操作和延时。

 

参考:http://www.automatedqa.com/techpapers/test_playback_performance_tips.asp

 

 

12、CheckPoint

Tip 3 - Checkpoints

As you’re recording your tests,you’ll want to verify that your application is behaving properly.TestComplete’s Checkpoints verify that: information has rendered properly onscreen; information has been successfully written to a database; a web servicereturns the correct value; and much more.

 

In Keyword test mode, there’s acheckpoint operations panel that lists all available checkpoints. Simply dragthe desired checkpoint onto the test panel and follow the prompts provided bythe wizard that’s displayed.

 

 

 

If you’re not sure which checkpointyou should use, there’s a CheckpointWizard that asks a series of simple questions to help youdecide which checkpoint is right for a given test. A series of videos thatdescribe some of TestComplete’s checkpoints can be seen here:
http://www.automatedqa.com/products/testcomplete/screencasts/

 

 

13、如何读写COM端口?

 

Sub TestCOMPort

 Const ForWriting = 2, TriStateFalse = 0

  Dimfso, f

 

  Setfso = CreateObject("Scripting.FileSystemObject")

  Setf = fso.OpenTextFile("COM1:", ForWriting, False, TriStateFalse)

  'Write data to the port

 f.Write Chr(26)

 f.Write Chr(32)

 f.Write Chr(27)

 f.Close

End Sub

 

 

or

 

 

Sub Test
  Dim Port, i, s

  Set Port =dotNET.System_IO_Ports.SerialPort.zctor_4("COM1", 9600)
  Port.Open

  ' Writing data to the port
  Port.Write "A " & Chr(27)
  ' Waiting for response
  aqUtils.Delay 1000
  ' Processing response
  If Port.BytesToRead <> 0 Then
    s = Port.ReadExisting
    Log.Message s
  Else
    Log.Warning "No data"
  End If

  Port.Close
End Sub

 

14、如何表示十六进制?

 

VBScript中可以用类似&HFF的格式表示十六进制数

 

 

15、怎么判断一个对象是否存在某个属性?

 

用aqObject.GetProperties

 

例:

Set p =Sys.Process("MyApplication")

Set props = aqObject.GetProperties(p)

 

While props.HasNext

  Setprop = props.Next

 Log.Message prop.Name & ": " & aqConvert.VarToStr(prop.Value)

Wend

 

 

16、如何执行ClickOnce应用程序?

 

http://www.automatedqa.com/support/viewarticle/?aid=17600

 

Sub RunClickOnceApplication()

  Dimpath 

 path = "<pathToTheApplication>" ' e.g. "c:\SampleClickOnce\sample.appref-ms"  SetWshShell = CreateObject("WScript.Shell")

 WshShell.Run("rundll32.exe dfshim.dll,ShOpenVerbShortcut "& path)

End Sub

 

 

17、如何获取当前测试日志?

 

http://www.automatedqa.com/support/viewarticle.aspx?aid=9047

 

Sub Test()

  Call Log.Message("Currentlog items", GetLogItems())

End Sub 

 

Function GetLogItems()

  Dim tempFolder, xDoc,result 

  tempFolder = aqEnvironment.GetEnvironmentVariable("temp")& "\" &_

  GetTickCount() & "\"

  If 0 <> aqFileSystem.CreateFolder(tempFolder)Then 

    Log.Error("The " &tempFolder & " temp folder was not created")   

    GetLogItems = ""

    Exit Function 

  End If 

  If Not Log.SaveResultsAs(tempFolder,lsHTML) Then   

    Log.Error("Log was not exported tothe " & tempFolder & " temp folder")

    GetLogItems = ""   

    Exit Function 

  End If 

  Set  xDoc = Sys.OleObject("MSXML2.DOMDocument.4.0") 

  xDoc.load(tempFolder &"root.xml")                                            

  result =LogDataToText(xDoc.childNodes.item(1), 0, "  ")   

  Call aqFileSystem.DeleteFolder(tempFolder,True

  GetLogItems = result

End Function

 

Function LogDataToText(logData, indentIndex,indentSymbol)  Dim i, result

  If "LogData"<> logData.nodeName Then 

    LogDataToText =""   

    Exit Function 

  End If

  result = "" 

  For i = 0 ToindentIndex - 1

    result = result &indentSymbol 

  Next 

  result = result & "Name:" & logData.getAttribute("name") & ", status:" &_            

 GetTextOfStatus(logData.getAttribute("status")) & vbCrLf

  For i = 0 TologData.childNodes.length - 1

    result = result &LogDataToText(logData.childNodes.item(i),_                                   

    indentIndex + 1, indentSymbol)

  Next 

  LogDataToText = result

End Function

 

Function GetTextOfStatus(statusIndex)  

  Select CasestatusIndex

    Case "0"GetTextOfStatus = "OK"   

    Case "2"GetTextOfStatus = "FAILED"

    Case ElseGetTextOfStatus = "UNDEFINED"

  End Select 

End Function 

 

 

18、如何检查COM对象是否已经注册?

 

http://www.automatedqa.com/support/viewarticle/?aid=17606

 

Sub Test1()

  Dim objName  

  objName = "C:\ProgramFiles\Automated QA\TestComplete 8\Bin\TestComplete.exe"

  If(IsCOMObjectRegistered("localhost", objectName)) Then   

  Call Log.Message("Anobject is registered")

  Else   

  Call Log.Error("Anobject was not registered")

  End If 

End Sub 

 

Function IsCOMObjectRegistered(computerName, objectPath)

  Dim wmiService,wmiObjectPath, objectsList 

  Set wmiService =GetObject("WinMgmts:{impersonationLevel=impersonate}!\\" &_

                              computerName & "\root\cimv2")

  wmiObjectPath =Replace(objectPath, "\", "\\") 

  Set objectsList = wmiService.ExecQuery("SELECT* FROM " &_

   "Win32_ClassicCOMClassSetting WHERE LocalServer32='" &wmiObjectPath & "'") 

  IsCOMObjectRegistered = False

  For Each ObjService inObjectsList   

    IsCOMObjectRegistered = True 

    Exit Function 

  Next 

End Function

 

 

19、如何通过命令行传入参数给TestComplete的脚本进行处理?

 

Pass parameters via the command line

http://www.automatedqa.com/support/viewarticle.aspx?aid=9021

 

Sub ProcessCommandLine

  For i = 1 To BuiltIn.ParamCount   

  ProcessCommandLineArgument BuiltIn.ParamStr(i)

  Next 

End Sub 

 

Sub ProcessCommandLineArgument(arg)

  Dim items 

  items = Split(arg,"=") 

  If UBound(items)<> 1 Then 

    Exit Sub 

  End If 

    Select Case aqString.ToLower(aqString.Trim(items(0)))

    Case"apppath"      'Project.Variables.AppPath= aqString.Trim(items(1)) 

      Log.Message "The 'apppath' argumentis found! The value is '"_

               & aqString.Trim(items(1))& "'"

    Case"appver"      'Project.Variables.AppVer= aqString.Trim(items(1)) 

      Log.Message "The 'appver' argumentis found! The value is '"_

               & aqString.Trim(items(1))& "'"

  End Select 

End Sub

 

 

20、处理非预期窗口的设置

 

TestComplete - Tip 8 (Handling UnexpectedWindows)

 

Each of these options is listed in your project’s properties panel, andyou can enable or disable any combination of them according to your uniqueneeds.

 

 

 

21、等待对象

 

Tip 9 Waiting for Objects

Sometimes you need to have your automated tests pause for a certainlength of time. Maybe you need to wait while your application reads informationfrom a database, or you need to wait for a file to download. TestCompleteoffers several ways for you to delay your tests execution for situations likethis.

First is the Delay command. Thispauses the test for a specific number of milliseconds. In keyword tests, youinsert delays by dragging the Delay operation from the miscellaneous group box.

 

 

 

In a script based test, you can simply enter the word Delay andenter the number of milliseconds you want to wait. Refer to the TestCompletehelp file’s Delay help topic for syntax examples of each language.

You can also pause test execution until a certain window or objectexists onscreen. This is accomplished by using built-in commands likeWaitWindow and WaitChild. A video demonstrating how to use these commands canbe found here: http://www.automatedqa.com/products/testcomplete/screencasts/waiting-objects/

 

22、如何扩展TestComplete

Tip 11Extensions
TestCompletewas designed to be extensible, and you can create your own custom checkpoints,scripting objects, and helper functions via a feature called Extensions. Theselet you transform complicated or lengthy operations into single clicksolutions. For example, you could create a checkpoint that verified zip codeswere formatted properly, or that temperatures in Celsius were successfullyconverted to Fahrenheit. There are several prebuilt extensions available on ourwebsite:

http://www.automatedqa.com/blogs/post/08-11-20/7-ready-made-test-extensions/

You can learn more about building custom extensions from this video:

http://www.automatedqa.com/products/testcomplete/screencasts/scriptextensions/

 

23、TestComplete设置断点不生效

 

可以尝试安装MicrosoftScript Debuger或重新注册PMD.dll,参考:

http://www.automatedqa.com/support/viewarticle/8874/

 

 

 

posted on 2010-05-07 10:45  TIB  阅读(2367)  评论(1编辑  收藏  举报

导航